Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
Size
114.5 KB
Version
1.1.53
Created
Dec 7, 2025
Updated
about 2 months ago
1// ==UserScript==
2// @name Ozon AI Analyzer 2.1
3// @description Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version 1.1.53
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
13 'use strict';
14
15
16 console.log('🚀 Ozon AI Аналитик Продаж запущен');
17
18
19 // Утилита для задержки
20
21 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
22
23
24 // Парсинг числовых значений
25
26 function parseNumber(str) {
27
28 if (!str || str === '—' || str === '') return null;
29
30 // Извлекаем первое число ДО знака процента
31
32 const match = str.match(/^([\d\s,.]+)/);
33
34 if (!match) return null;
35
36 // Убираем пробелы (разделители тысяч), потом парсим
37
38 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
39
40 const num = parseFloat(cleaned);
41
42 return isNaN(num) ? null : num;
43
44 }
45
46
47 // Парсинг процентов
48
49 function parsePercent(str) {
50
51 if (!str || str === '—' || str === '') return null;
52
53 const match = str.match(/([+-]?\d+(?:\.\d+)?)\s*%/);
54
55 return match ? parseFloat(match[1]) : null;
56
57 }
58
59
60 // Парсинг цены (убираем пробелы между цифрами)
61
62 function parsePrice(str) {
63
64 if (!str || str === '—' || str === '') return null;
65
66 // Извлекаем первое число ДО знака процента
67
68 const match = str.match(/^([\d\s,.]+)/);
69
70 if (!match) return null;
71
72 // Убираем пробелы, затем все кроме цифр и точек
73
74 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
75
76 const num = parseFloat(cleaned);
77
78 return isNaN(num) ? null : num;
79
80 }
81
82
83 // Таблица с данными для расчета прибыли
84
85 const PRODUCT_COST_DATA = {
86
87 '72252': { cost: 158.4, commission: 0.27, delivery: 90 },
88
89 '71613': { cost: 108, commission: 0.27, delivery: 90 },
90
91 '73716': { cost: 126, commission: 0.27, delivery: 90 },
92
93 '80036': { cost: 103.2, commission: 0.27, delivery: 90 },
94
95 '73365': { cost: 166.8, commission: 0.27, delivery: 90 },
96
97 '74881': { cost: 135.6, commission: 0.27, delivery: 90 },
98
99 '73266': { cost: 708, commission: 0.27, delivery: 90 },
100
101 '73655': { cost: 219.6, commission: 0.27, delivery: 90 },
102
103 '75222': { cost: 103.2, commission: 0.27, delivery: 90 },
104
105 '73358': { cost: 163.2, commission: 0.27, delivery: 90 },
106
107 '73723': { cost: 116.4, commission: 0.27, delivery: 90 },
108
109 '74119': { cost: 110.4, commission: 0.27, delivery: 90 },
110
111 '72573': { cost: 146.4, commission: 0.27, delivery: 90 },
112
113 '72221': { cost: 90, commission: 0.27, delivery: 90 },
114
115 '75345': { cost: 111.6, commission: 0.27, delivery: 90 },
116
117 '73334': { cost: 104.4, commission: 0.27, delivery: 90 },
118
119 '72184': { cost: 177.6, commission: 0.27, delivery: 90 },
120
121 '75291': { cost: 100.8, commission: 0.27, delivery: 90 },
122
123 '73655': { cost: 163.2, commission: 0.27, delivery: 90 },
124
125 '73976': { cost: 170.4, commission: 0.27, delivery: 90 },
126
127 '75710': { cost: 172.8, commission: 0.27, delivery: 90 },
128
129 '76113': { cost: 115.2, commission: 0.27, delivery: 90 },
130
131 '75499': { cost: 96, commission: 0.27, delivery: 90 },
132
133 '71569': { cost: 117.6, commission: 0.27, delivery: 90 },
134
135 '72276': { cost: 286.8, commission: 0.27, delivery: 90 },
136
137 '75338': { cost: 90, commission: 0.27, delivery: 90 },
138
139 '75536': { cost: 87.6, commission: 0.27, delivery: 90 },
140
141 '76014': { cost: 135.6, commission: 0.27, delivery: 90 },
142
143 '73730': { cost: 121.2, commission: 0.27, delivery: 90 },
144
145 '75628': { cost: 124.8, commission: 0.27, delivery: 90 },
146
147 '74249': { cost: 312, commission: 0.27, delivery: 90 },
148
149 '75468': { cost: 138, commission: 0.27, delivery: 90 },
150
151 '73495': { cost: 120, commission: 0.27, delivery: 90 },
152
153 '74393': { cost: 126, commission: 0.27, delivery: 90 },
154
155 '74188': { cost: 174, commission: 0.27, delivery: 90 },
156
157 '73907': { cost: 155.376, commission: 0.27, delivery: 90 },
158
159 '73396': { cost: 112.8, commission: 0.27, delivery: 90 },
160
161 '71668': { cost: 96, commission: 0.27, delivery: 90 },
162
163 '73235': { cost: 114, commission: 0.27, delivery: 90 },
164
165 '75093': { cost: 96, commission: 0.27, delivery: 90 },
166
167 '73891': { cost: 100.8, commission: 0.27, delivery: 90 },
168
169 '75505': { cost: 91.2, commission: 0.27, delivery: 90 },
170
171 '71590': { cost: 100.8, commission: 0.27, delivery: 90 },
172
173 '73488': { cost: 150, commission: 0.27, delivery: 90 },
174
175 '75413': { cost: 128.4, commission: 0.27, delivery: 90 },
176
177 '76403': { cost: 352.8, commission: 0.27, delivery: 90 },
178
179 '74799': { cost: 162, commission: 0.27, delivery: 90 },
180
181 '75406': { cost: 117.6, commission: 0.27, delivery: 90 },
182
183 '75154': { cost: 123.6, commission: 0.27, delivery: 90 },
184
185 '75383': { cost: 82.8, commission: 0.27, delivery: 90 },
186
187 '80029': { cost: 67.416, commission: 0.27, delivery: 90 },
188
189 '76120': { cost: 264, commission: 0.27, delivery: 90 },
190
191 '72306': { cost: 186, commission: 0.27, delivery: 90 },
192
193 '75246': { cost: 88.8, commission: 0.27, delivery: 90 },
194
195 '73228': { cost: 133.476, commission: 0.27, delivery: 90 },
196
197 '73419': { cost: 165.6, commission: 0.27, delivery: 90 },
198
199 '74379': { cost: 175.2, commission: 0.27, delivery: 90 },
200
201 '83356': { cost: 229.2, commission: 0.27, delivery: 90 },
202
203 '75444': { cost: 123.6, commission: 0.27, delivery: 90 },
204
205 '79992': { cost: 127.2, commission: 0.27, delivery: 90 },
206
207 '73709': { cost: 218.4, commission: 0.27, delivery: 90 },
208
209 '73778': { cost: 144, commission: 0.27, delivery: 90 },
210
211 '72269': { cost: 194.4, commission: 0.27, delivery: 90 },
212
213 '73440': { cost: 118.8, commission: 0.27, delivery: 90 },
214
215 '74669': { cost: 176.4, commission: 0.27, delivery: 90 },
216
217 '77660': { cost: 0, commission: 0.27, delivery: 90 },
218
219 '77578': { cost: 0, commission: 0.27, delivery: 90 },
220
221 '71545': { cost: 124.8, commission: 0.27, delivery: 90 },
222
223 '75673': { cost: 97.2, commission: 0.27, delivery: 90 },
224
225 '76168': { cost: 80.4, commission: 0.27, delivery: 90 },
226
227 '75277': { cost: 217.2, commission: 0.27, delivery: 90 },
228
229 '75390': { cost: 108, commission: 0.27, delivery: 90 },
230
231 '74263': { cost: 160.8, commission: 0.27, delivery: 90 },
232
233 '74676': { cost: 176.4, commission: 0.27, delivery: 90 },
234
235 '75727': { cost: 121.2, commission: 0.27, delivery: 90 },
236
237 '74126': { cost: 111.6, commission: 0.27, delivery: 90 },
238
239 '74294': { cost: 145.2, commission: 0.27, delivery: 90 },
240
241 '76069': { cost: 220.8, commission: 0.27, delivery: 90 },
242
243 '71361': { cost: 204, commission: 0.27, delivery: 90 },
244
245 '73501': { cost: 114, commission: 0.27, delivery: 90 },
246
247 '72238': { cost: 102, commission: 0.27, delivery: 90 },
248
249 '75482': { cost: 218.4, commission: 0.27, delivery: 90 },
250
251 '76489': { cost: 216, commission: 0.27, delivery: 90 },
252
253 '76076': { cost: 103.2, commission: 0.27, delivery: 90 },
254
255 '75437': { cost: 69.6, commission: 0.27, delivery: 90 },
256
257 '75352': { cost: 72, commission: 0.27, delivery: 90 },
258
259 '75550': { cost: 112.8, commission: 0.27, delivery: 90 },
260
261 '75529': { cost: 114, commission: 0.27, delivery: 90 },
262
263 '76021': { cost: 243.6, commission: 0.27, delivery: 90 },
264
265 '73969': { cost: 91.2, commission: 0.27, delivery: 90 },
266
267 '73242': { cost: 133.488, commission: 0.27, delivery: 90 },
268
269 '80111': { cost: 58.8, commission: 0.27, delivery: 90 },
270
271 '73693': { cost: 159.6, commission: 0.27, delivery: 90 },
272
273 '75703': { cost: 208.8, commission: 0.27, delivery: 90 },
274
275 '74980': { cost: 158.4, commission: 0.27, delivery: 90 },
276
277 '76380': { cost: 145.6, commission: 0.27, delivery: 90 },
278
279 '77677': { cost: 129.948, commission: 0.27, delivery: 90 },
280
281 '75369': { cost: 103.2, commission: 0.27, delivery: 90 },
282
283 '74713': { cost: 180, commission: 0.27, delivery: 90 },
284
285 '75024': { cost: 114, commission: 0.27, delivery: 90 },
286
287 '77615': { cost: 0, commission: 0.27, delivery: 90 },
288
289 '73389': { cost: 136.8, commission: 0.27, delivery: 90 },
290
291 '74850': { cost: 169.2, commission: 0.27, delivery: 90 },
292
293 '75192': { cost: 180, commission: 0.27, delivery: 90 },
294
295 '73310': { cost: 140.4, commission: 0.27, delivery: 90 },
296
297 '73280': { cost: 81.6, commission: 0.27, delivery: 90 },
298
299 '75048': { cost: 122.4, commission: 0.27, delivery: 90 },
300
301 '74874': { cost: 140.4, commission: 0.27, delivery: 90 },
302
303 '71675': { cost: 105.6, commission: 0.27, delivery: 90 },
304
305 '74225': { cost: 153.6, commission: 0.27, delivery: 90 },
306
307 '74768': { cost: 117.6, commission: 0.27, delivery: 90 },
308
309 '73136': { cost: 163.2, commission: 0.27, delivery: 90 },
310
311 '74300': { cost: 134.4, commission: 0.27, delivery: 90 },
312
313 '76410': { cost: 328.8, commission: 0.27, delivery: 90 },
314
315 '74898': { cost: 139.2, commission: 0.27, delivery: 90 },
316
317 '73129': { cost: 159.6, commission: 0.27, delivery: 90 },
318
319 '75253': { cost: 117.6, commission: 0.27, delivery: 90 },
320
321 '75642': { cost: 144.54, commission: 0.27, delivery: 90 },
322
323 '75512': { cost: 88.8, commission: 0.27, delivery: 90 },
324
325 '70999': { cost: 164.4, commission: 0.27, delivery: 90 },
326
327 '76137': { cost: 103.788, commission: 0.27, delivery: 90 },
328
329 '74072': { cost: 148.8, commission: 0.27, delivery: 90 },
330
331 '73297': { cost: 85.2, commission: 0.27, delivery: 90 },
332
333 '76465': { cost: 301.452, commission: 0.27, delivery: 90 },
334
335 '71835': { cost: 73.2, commission: 0.27, delivery: 90 },
336
337 '74324': { cost: 129.6, commission: 0.27, delivery: 90 },
338
339 '71644': { cost: 132, commission: 0.27, delivery: 90 },
340
341 '75420': { cost: 106.8, commission: 0.27, delivery: 90 },
342
343 '74355': { cost: 182.4, commission: 0.27, delivery: 90 },
344
345 '71651': { cost: 174, commission: 0.27, delivery: 90 },
346
347 '74973': { cost: 144, commission: 0.27, delivery: 90 },
348
349 '73341': { cost: 130.8, commission: 0.27, delivery: 90 },
350
351 '75185': { cost: 157.2, commission: 0.27, delivery: 90 },
352
353 '74348': { cost: 132, commission: 0.27, delivery: 90 },
354
355 '75376': { cost: 69.6, commission: 0.27, delivery: 90 },
356
357 '74942': { cost: 159.6, commission: 0.27, delivery: 90 },
358
359 '77592': { cost: 0, commission: 0.27, delivery: 90 },
360
361 '74737': { cost: 183.6, commission: 0.27, delivery: 90 },
362
363 '76045': { cost: 235.2, commission: 0.27, delivery: 90 },
364
365 '74256': { cost: 186, commission: 0.27, delivery: 90 },
366
367 '75208': { cost: 200.4, commission: 0.27, delivery: 90 },
368
369 '76601': { cost: 313.2, commission: 0.27, delivery: 90 },
370
371 '75116': { cost: 346.8, commission: 0.27, delivery: 90 },
372
373 '73464': { cost: 258, commission: 0.27, delivery: 90 },
374
375 '74577': { cost: 134.4, commission: 0.27, delivery: 90 },
376
377 '73792': { cost: 120, commission: 0.27, delivery: 90 },
378
379 '74997': { cost: 159.6, commission: 0.27, delivery: 90 },
380
381 '75611': { cost: 150, commission: 0.27, delivery: 90 },
382
383 '74782': { cost: 145.2, commission: 0.27, delivery: 90 },
384
385 '75031': { cost: 87.6, commission: 0.27, delivery: 90 },
386
387 '74195': { cost: 171.6, commission: 0.27, delivery: 90 },
388
389 '75161': { cost: 96, commission: 0.27, delivery: 90 },
390
391 '74591': { cost: 132, commission: 0.27, delivery: 90 },
392
393 '20107': { cost: 283.752, commission: 0.27, delivery: 90 },
394
395 '74935': { cost: 331.2, commission: 0.27, delivery: 90 },
396
397 '75062': { cost: 99.6, commission: 0.27, delivery: 90 },
398
399 '74706': { cost: 130.8, commission: 0.27, delivery: 90 },
400
401 '75147': { cost: 470.4, commission: 0.27, delivery: 90 },
402
403 '73181': { cost: 234, commission: 0.27, delivery: 90 },
404
405 '74812': { cost: 366, commission: 0.27, delivery: 90 },
406
407 '74805': { cost: 421.2, commission: 0.27, delivery: 90 },
408
409 '74010': { cost: 112.8, commission: 0.27, delivery: 90 },
410
411 '73167': { cost: 98.4, commission: 0.27, delivery: 90 },
412
413 '74416': { cost: 91.2, commission: 0.27, delivery: 90 },
414
415 '75574': { cost: 1517, commission: 0.27, delivery: 90 },
416
417 '74546': { cost: 240, commission: 0.27, delivery: 90 },
418
419 '76199': { cost: 271.2, commission: 0.27, delivery: 90 },
420
421 '74829': { cost: 372, commission: 0.27, delivery: 90 },
422
423 '80173': { cost: 768, commission: 0.27, delivery: 90 },
424
425 '75567': { cost: 1497, commission: 0.27, delivery: 90 },
426
427 '73754': { cost: 392.4, commission: 0.27, delivery: 90 },
428
429 '76298': { cost: 637.2, commission: 0.27, delivery: 90 },
430
431 '74454': { cost: 88.8, commission: 0.27, delivery: 90 },
432
433 '76205': { cost: 273.6, commission: 0.27, delivery: 90 },
434
435 '76274': { cost: 662.4, commission: 0.27, delivery: 90 },
436
437 '76441': { cost: 124.8, commission: 0.27, delivery: 90 },
438
439 '76434': { cost: 145.2, commission: 0.27, delivery: 90 },
440
441 '80227': { cost: 768, commission: 0.27, delivery: 90 },
442
443 '76175': { cost: 285.6, commission: 0.27, delivery: 90 },
444
445 '76304': { cost: 324, commission: 0.27, delivery: 90 },
446
447 '71682': { cost: 99.6, commission: 0.27, delivery: 90 },
448
449 '74959': { cost: 183.6, commission: 0.27, delivery: 90 },
450
451 '73198': { cost: 120, commission: 0.27, delivery: 90 }
452
453 };
454
455
456 // Функция расчета прибыли
457
458 function calculateProfit(article, revenue, orders, drr) {
459
460 const costData = PRODUCT_COST_DATA[article];
461
462 if (!costData || !revenue || !orders) return null;
463
464
465
466 // Расходы на рекламу = выручка * (ДРР / 100)
467
468 const adCost = drr ? (revenue * (drr / 100)) : 0;
469
470
471
472 // Прибыль = Выручка - (заказы * себестоимость) - (заказы * доставка) - (выручка * комиссия) - расходы на рекламу
473
474 const profit = revenue - (orders * costData.cost) - (orders * costData.delivery) - (revenue * costData.commission) - adCost;
475
476 return Math.round(profit); // Округляем до целых
477
478 }
479
480
481 // Класс для сбора данных о товарах
482
483 class ProductDataCollector {
484
485 constructor() {
486
487 this.products = [];
488
489 this.isCollecting = false;
490
491 this.analysisPeriodDays = 7; // По умолчанию 7 дней
492
493 }
494
495
496 // Определение периода анализа из интерфейса
497 detectAnalysisPeriod() {
498 try {
499 console.log('🔍 Ищем период анализа в интерфейсе...');
500
501 // ПРИОРИТЕТ 1: Проверяем активную кнопку периода
502 // Ищем кнопки с текстом периода (Сегодня, Вчера, 7 дней и т.д.)
503 const periodButtons = document.querySelectorAll('button[data-active="true"] span.body-500');
504
505 for (const button of periodButtons) {
506 const buttonText = button.textContent.trim();
507 console.log(`🔘 Проверяем кнопку: "${buttonText}"`);
508
509 // Маппинг кнопок на количество дней
510 const periodMap = {
511 'Сегодня': 1,
512 'Вчера': 1,
513 '7 дней': 7,
514 '28 дней': 28,
515 'Квартал': 90,
516 'Год': 365
517 };
518
519 if (periodMap[buttonText]) {
520 this.analysisPeriodDays = periodMap[buttonText];
521 console.log(`✅ Определен период по кнопке: ${this.analysisPeriodDays} дней (кнопка: "${buttonText}")`);
522 return this.analysisPeriodDays;
523 }
524 }
525
526 // ПРИОРИТЕТ 2: Ищем все текстовые элементы на странице
527 const allText = document.body.innerText;
528
529 // Паттерн 1: "30 нояб — 6 дек" или "9 нояб — 6 дек" или "07 сент — 6 дек"
530 let dateRangeMatch = allText.match(/(\d{1,2})\s*(янв|фев|мар|апр|мая|май|июн|июл|авг|сен|окт|ноя|дек)[а-я]*\s*[-—–]\s*(\d{1,2})\s*(янв|фев|мар|апр|мая|май|июн|июл|авг|сен|окт|ноя|дек)[а-я]*/i);
531
532 if (dateRangeMatch) {
533 const startDay = parseInt(dateRangeMatch[1]);
534 const endDay = parseInt(dateRangeMatch[3]);
535 const startMonth = dateRangeMatch[2].toLowerCase();
536 const endMonth = dateRangeMatch[4].toLowerCase();
537
538 console.log(`📅 Найдены даты: ${startDay} ${startMonth} — ${endDay} ${endMonth}`);
539
540 // Маппинг месяцев
541 const monthMap = {
542 'янв': 1, 'фев': 2, 'мар': 3, 'апр': 4, 'мая': 5, 'май': 5,
543 'июн': 6, 'июл': 7, 'авг': 8, 'сен': 9, 'окт': 10, 'ноя': 11, 'дек': 12
544 };
545
546 const startMonthNum = monthMap[startMonth.substring(0, 3)];
547 const endMonthNum = monthMap[endMonth.substring(0, 3)];
548
549 console.log(`📅 Месяцы: ${startMonthNum} → ${endMonthNum}`);
550
551 let days;
552
553 if (startMonthNum === endMonthNum) {
554 // Даты в одном месяце
555 days = endDay - startDay + 1;
556 console.log(`📅 Один месяц: ${days} дней`);
557 } else {
558 // Разные месяцы - считаем точно
559 const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
560
561 // Дни от начальной даты до конца начального месяца
562 const daysInStartMonth = daysInMonth[startMonthNum - 1];
563 const daysFromStart = daysInStartMonth - startDay + 1;
564
565 // Дни полных месяцев между начальным и конечным
566 let daysInBetween = 0;
567 let currentMonth = startMonthNum;
568 while (true) {
569 currentMonth++;
570 if (currentMonth > 12) currentMonth = 1; // Переход через год
571 if (currentMonth === endMonthNum) break;
572 daysInBetween += daysInMonth[currentMonth - 1];
573 }
574
575 // Дни конечного месяца
576 const daysInEndMonth = endDay;
577
578 days = daysFromStart + daysInBetween + daysInEndMonth;
579 console.log(`📅 Несколько месяцев: ${daysFromStart} + ${daysInBetween} + ${daysInEndMonth} = ${days} дней`);
580 }
581
582 if (days > 0 && days <= 365) {
583 this.analysisPeriodDays = days;
584 console.log(`✅ Определен период анализа: ${days} дней`);
585 return days;
586 }
587 }
588
589 // Паттерн 3: Ищем текст типа "за 7 дней", "за 14 дней", "за 28 дней"
590 const periodMatch = allText.match(/за\s+(\d+)\s+дн/i);
591 if (periodMatch) {
592 const days = parseInt(periodMatch[1]);
593 if (days > 0 && days <= 365) {
594 this.analysisPeriodDays = days;
595 console.log(`✅ Определен период анализа: ${days} дней`);
596 return days;
597 }
598 }
599
600 console.log(`⚠️ Период анализа не определен, используем по умолчанию: ${this.analysisPeriodDays} дней`);
601 return this.analysisPeriodDays;
602 } catch (error) {
603 console.error('Ошибка определения периода:', error);
604 return this.analysisPeriodDays;
605 }
606 }
607
608
609 // Автоматическая подгрузка всех товаров
610
611 async loadAllProducts() {
612
613 console.log('📦 Начинаем загрузку всех товаров...');
614
615 this.isCollecting = true;
616
617
618 let previousCount = 0;
619
620 let stableCount = 0; // Счетчик стабильных попыток
621
622 let attempts = 0;
623
624 const maxAttempts = 300; // Увеличили максимум попыток до 300
625
626 const maxStableAttempts = 5; // Увеличили до 5 стабильных попыток
627
628
629 while (attempts < maxAttempts) {
630
631 const loadMoreBtn = document.querySelector('button.styles_loadMoreButton_2RI3D');
632
633
634
635 if (!loadMoreBtn) {
636
637 console.log('✅ Кнопка "Показать ещё" не найдена - все товары загружены');
638
639 break;
640
641 }
642
643
644 // Проверяем, не отключена ли кнопка
645
646 if (loadMoreBtn.disabled || loadMoreBtn.classList.contains('disabled')) {
647
648 console.log('✅ Кнопка "Показать ещё" отключена - все товары загружены');
649
650 break;
651
652 }
653
654
655 // Проверяем, есть ли товары с нулевой выручкой (значит дошли до конца)
656
657 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
658
659 let hasZeroRevenue = false;
660
661
662
663 for (const row of rows) {
664
665 const cells = row.querySelectorAll('td');
666
667 if (cells.length >= 3) {
668
669 const revenueText = cells[2].textContent.trim();
670
671 // Проверяем, есть ли "0 ₽" или "0₽" в тексте выручки
672
673 if (revenueText.match(/^0\s*₽/) || revenueText === '0') {
674
675 hasZeroRevenue = true;
676
677 console.log('✅ Найден товар с нулевой выручкой - останавливаем загрузку');
678
679 break;
680
681 }
682
683 }
684
685 }
686
687
688
689 if (hasZeroRevenue) {
690
691 console.log('✅ Достигнут конец списка активных товаров');
692
693 break;
694
695 }
696
697
698 // Прокручиваем к кнопке, чтобы она была видна
699
700 loadMoreBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
701
702 await delay(800);
703
704
705 console.log(`🔄 Клик по кнопке "Показать ещё" (попытка ${attempts + 1})`);
706
707 loadMoreBtn.click();
708
709
710
711 // Увеличили задержку до 4 секунд для полной загрузки данных
712
713 await delay(4000);
714
715
716 const currentCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
717
718 console.log(`📊 Загружено товаров: ${currentCount} (было: ${previousCount})`);
719
720
721 if (currentCount === previousCount) {
722
723 stableCount++;
724
725 console.log(`⏸️ Количество не изменилось (${stableCount}/${maxStableAttempts})`);
726
727
728
729 if (stableCount >= maxStableAttempts) {
730
731 console.log('✅ Количество товаров стабильно - загрузка завершена');
732
733 break;
734
735 }
736
737 } else {
738
739 stableCount = 0; // Сбрасываем счетчик, если количество изменилось
740
741 }
742
743
744 previousCount = currentCount;
745
746 attempts++;
747
748 }
749
750
751 const finalCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
752
753 console.log(`✅ Загрузка завершена. Всего товаров: ${finalCount}`);
754
755 this.isCollecting = false;
756
757 }
758
759
760 // Сбор данных из таблицы
761
762 collectProductData() {
763
764 console.log('📊 Собираем данные о товарах...');
765
766 this.products = [];
767
768
769 // Определяем период анализа
770 this.detectAnalysisPeriod();
771
772
773 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
774
775 console.log(`Найдено строк: ${rows.length}`);
776
777
778 rows.forEach((row, index) => {
779
780 try {
781
782 const cells = row.querySelectorAll('td');
783
784 if (cells.length < 10) return;
785
786
787 // Извлекаем данные из ячеек
788
789 const productData = this.extractProductData(cells);
790
791 if (productData) {
792
793 this.products.push(productData);
794
795 }
796
797 } catch (error) {
798
799 console.error(`Ошибка при обработке строки ${index}:`, error);
800
801 }
802
803 });
804
805
806 console.log(`✅ Собрано товаров: ${this.products.length}`);
807
808 return this.products;
809
810 }
811
812
813 // Извлечение данных о товаре из ячеек
814
815 extractProductData(cells) {
816
817 try {
818
819 // Название и артикул (первая ячейка)
820
821 const nameCell = cells[0];
822
823 const nameLink = nameCell.querySelector('a.styles_productName_2qRJi');
824
825 const captionEl = nameCell.querySelector('.styles_productCaption_7MqtH');
826
827
828
829 const name = nameLink ? nameLink.textContent.trim() : '';
830
831 const articleMatch = captionEl ? captionEl.textContent.match(/Арт\.\s*(\d+)/) : null;
832
833 const article = articleMatch ? articleMatch[1] : '';
834
835
836 if (!name || !article) return null;
837
838
839 // Получаем текстовое содержимое всех ячеек
840
841 const cellTexts = Array.from(cells).map(cell => cell.textContent.trim());
842
843
844 // Парсим основные показатели по правильным индексам
845
846 // Выручка - индекс 2
847
848 const revenue = parseNumber(cellTexts[2]);
849
850 const revenueChange = parsePercent(cellTexts[2]);
851
852
853
854 // Заказано товаров - индекс 20
855
856 const orders = parseNumber(cellTexts[20]);
857
858 const ordersChange = parsePercent(cellTexts[20]);
859
860
861
862 // Показы всего - индекс 5
863
864 const impressions = parseNumber(cellTexts[5]);
865
866 const impressionsChange = parsePercent(cellTexts[5]);
867
868
869
870 // Посещения карточки товара - индекс 13
871
872 const cardVisits = parseNumber(cellTexts[13]);
873
874 const cardVisitsChange = parsePercent(cellTexts[13]);
875
876
877
878 // Конверсия из поиска и каталога в карточку (CTR) - индекс 12
879
880 const conversionCatalogToCard = parseNumber(cellTexts[12]);
881
882 const conversionCatalogToCardChange = parsePercent(cellTexts[12]);
883
884
885
886 // Конверсия из карточки в корзину (CRL) - индекс 15
887
888 const conversionCardToCart = parseNumber(cellTexts[15]);
889
890 const conversionCardToCartChange = parsePercent(cellTexts[15]);
891
892
893
894 // Добавления в корзину всего - индекс 18
895
896 const cartAdditions = parseNumber(cellTexts[18]);
897
898 const cartAdditionsChange = parsePercent(cellTexts[18]);
899
900
901
902 // CR - высчитываем: Заказано товаров / Посещения карточки товаров
903
904 const cr = (orders && cardVisits && cardVisits > 0) ? parseFloat(((orders / cardVisits) * 100).toFixed(1)) : null;
905
906 const crChange = null; // Изменение CR нужно высчитывать отдельно
907
908
909
910 // Общая ДРР - индекс 32 (парсим как процент, убираем знак %)
911
912 const drrText = cellTexts[32] || '';
913
914 const drrMatch = drrText.match(/(\d+(?:\.\d+)?)\s*%/);
915
916 const drr = drrMatch ? parseFloat(drrMatch[1]) : null;
917
918 const drrChange = parsePercent(cellTexts[32]);
919
920
921
922 // Остаток на конец периода - индекс 35
923
924 const stockText = cellTexts[35] || '';
925
926 const stockMatch = stockText.match(/(\d+)/);
927
928 const stock = stockMatch ? parseInt(stockMatch[1]) : null;
929
930
931
932 // Средняя цена - индекс 28 (используем parsePrice для корректного парсинга)
933
934 const avgPrice = parsePrice(cellTexts[28]);
935
936 const avgPriceChange = parsePercent(cellTexts[28]);
937
938
939
940 // Среднее время доставки - индекс 37
941
942 const deliveryTime = cellTexts[37] || null;
943
944
945
946 // Рассчитываем прибыль
947
948 const profit = calculateProfit(article, revenue, orders, drr);
949
950
951
952 // Рассчитываем прибыль в процентах от выручки
953 const profitPercent = (profit !== null && revenue && revenue > 0) ?
954 parseFloat(((profit / revenue) * 100).toFixed(1)) : null;
955
956 // Рассчитываем комиссию и себестоимость
957 const costData = PRODUCT_COST_DATA[article];
958 const totalCommission = costData && revenue ?
959 Math.round((orders * costData.delivery) + (revenue * costData.commission)) : null;
960 const totalCommissionPercent = (totalCommission !== null && revenue && revenue > 0) ?
961 parseFloat(((totalCommission / revenue) * 100).toFixed(1)) : null;
962
963 const totalCost = costData && orders ? Math.round(orders * costData.cost) : null;
964 const totalCostPercent = (totalCost !== null && revenue && revenue > 0) ?
965 parseFloat(((totalCost / revenue) * 100).toFixed(1)) : null;
966
967 // ИСПРАВЛЕНИЕ: Рассчитываем "на дней" правильно
968 // Для периода в 1 день (Вчера/Сегодня): остаток / заказы = дней
969 // Для периода > 1 дня: остаток / (заказы / период) = дней
970 const daysOfStock = (orders && stock !== null && orders > 0) ?
971 Math.floor(stock / (orders / this.analysisPeriodDays)) : null;
972
973 // Логируем расчет для отладки
974 console.log(`📊 Артикул ${article}: остаток=${stock}, заказы=${orders}, период=${this.analysisPeriodDays} дней, на дней=${daysOfStock}`);
975
976
977 const product = {
978
979 name,
980
981 article,
982
983 revenue,
984
985 revenueChange,
986
987 orders,
988
989 ordersChange,
990
991 impressions,
992
993 impressionsChange,
994
995 cardVisits,
996
997 cardVisitsChange,
998
999 conversionCatalogToCard,
1000
1001 conversionCatalogToCardChange,
1002
1003 conversionCardToCart,
1004
1005 conversionCardToCartChange,
1006
1007 cartAdditions,
1008
1009 cartAdditionsChange,
1010
1011 cr,
1012
1013 crChange,
1014
1015 avgPrice,
1016
1017 avgPriceChange,
1018
1019 drr,
1020
1021 drrChange,
1022
1023 stock,
1024
1025 deliveryTime,
1026
1027 daysOfStock,
1028
1029 profit,
1030
1031 profitPercent,
1032
1033 totalCommission,
1034
1035 totalCommissionPercent,
1036
1037 totalCost,
1038
1039 totalCostPercent,
1040
1041 rawData: cellTexts
1042
1043 };
1044
1045
1046 return product;
1047
1048 } catch (error) {
1049
1050 console.error('Ошибка извлечения данных товара:', error);
1051
1052 return null;
1053
1054 }
1055
1056 }
1057
1058 }
1059
1060
1061 // Класс для AI анализа
1062
1063 class AIAnalyzer {
1064
1065 // Батч-анализ товаров с умной фильтрацией
1066
1067 async analyzeProducts(products, onProgress) {
1068
1069 console.log('🤖 Начинаем AI анализ товаров...');
1070
1071
1072
1073 // Сначала вычисляем средние показатели
1074
1075 const avgMetrics = this.calculateAverageMetrics(products);
1076
1077 console.log('📊 Средние показатели:', avgMetrics);
1078
1079
1080
1081 // Разделяем товары на приоритетные и обычные
1082
1083 const priorityProducts = [];
1084
1085 const normalProducts = [];
1086
1087
1088
1089 products.forEach(product => {
1090
1091 const needsAIAnalysis = this.needsDetailedAnalysis(product, avgMetrics);
1092
1093 if (needsAIAnalysis) {
1094
1095 priorityProducts.push(product);
1096
1097 } else {
1098
1099 normalProducts.push(product);
1100
1101 }
1102
1103 });
1104
1105
1106
1107 console.log(`📊 Приоритетных товаров для AI анализа: ${priorityProducts.length}`);
1108
1109 console.log(`📊 Обычных товаров (базовый анализ): ${normalProducts.length}`);
1110
1111
1112
1113 const analyzedProducts = [];
1114
1115 const batchSize = 10; // Увеличили до 10 товаров одновременно
1116
1117
1118
1119 // Сначала быстро обрабатываем обычные товары (без AI)
1120
1121 normalProducts.forEach(product => {
1122
1123 analyzedProducts.push({
1124
1125 ...product,
1126
1127 analysis: this.basicAnalysis(product, avgMetrics)
1128
1129 });
1130
1131 });
1132
1133
1134
1135 // Обновляем прогресс после базового анализа
1136
1137 if (onProgress) {
1138
1139 const percentage = Math.round((normalProducts.length / products.length) * 100);
1140
1141 const remaining = Math.ceil((priorityProducts.length / batchSize) * 2);
1142
1143 onProgress(normalProducts.length, products.length, percentage, remaining);
1144
1145 }
1146
1147
1148
1149 // Анализируем приоритетные товары с AI
1150
1151 for (let i = 0; i < priorityProducts.length; i += batchSize) {
1152
1153 const batch = priorityProducts.slice(i, i + batchSize);
1154
1155 const batchPromises = batch.map(product => this.analyzeProduct(product, avgMetrics, true));
1156
1157
1158
1159 const batchResults = await Promise.all(batchPromises);
1160
1161
1162
1163 batchResults.forEach((analysis, idx) => {
1164
1165 analyzedProducts.push({
1166
1167 ...batch[idx],
1168
1169 analysis
1170
1171 });
1172
1173 });
1174
1175
1176
1177 const progress = Math.min(i + batchSize, priorityProducts.length);
1178
1179 const totalProgress = normalProducts.length + progress;
1180
1181 const percentage = Math.round((totalProgress / products.length) * 100);
1182
1183 const remaining = Math.ceil(((priorityProducts.length - progress) / batchSize) * 2);
1184
1185
1186
1187 if (onProgress) {
1188
1189 onProgress(totalProgress, products.length, percentage, remaining);
1190
1191 }
1192
1193
1194
1195 console.log(`✅ Проанализировано ${progress} из ${priorityProducts.length} приоритетных товаров`);
1196
1197 }
1198
1199
1200
1201 if (onProgress) {
1202
1203 onProgress(products.length, products.length, 100, 0);
1204
1205 }
1206
1207
1208 return analyzedProducts;
1209
1210 }
1211
1212
1213 // Определяем, нужен ли детальный AI анализ
1214
1215 needsDetailedAnalysis(product, avgMetrics) {
1216
1217 const threshold = 5; // Порог отклонения 5%
1218
1219
1220
1221 // Если есть значительное падение выручки
1222
1223 if (product.revenueChange !== null && product.revenueChange < avgMetrics.revenueChange - threshold) {
1224
1225 return true;
1226
1227 }
1228
1229
1230
1231 // Если есть значительное падение заказов
1232
1233 if (product.ordersChange !== null && product.ordersChange < avgMetrics.ordersChange - threshold) {
1234
1235 return true;
1236
1237 }
1238
1239
1240
1241 // Если высокий ДРР
1242
1243 if (product.drr !== null && product.drr > 20) {
1244
1245 return true;
1246
1247 }
1248
1249
1250
1251 // Если низкие остатки
1252
1253 const daysOfStock = product.daysOfStock;
1254
1255 if (daysOfStock !== null && daysOfStock < 49) {
1256
1257 return true;
1258
1259 }
1260
1261
1262
1263 // Если значительный рост (для масштабирования)
1264
1265 if (product.revenueChange !== null && product.revenueChange > avgMetrics.revenueChange + 15) {
1266
1267 return true;
1268
1269 }
1270
1271
1272
1273 return false;
1274
1275 }
1276
1277
1278 // Базовый анализ без AI (для товаров без проблем)
1279
1280 basicAnalysis(product, avgMetrics) {
1281
1282 const daysOfStock = product.daysOfStock;
1283
1284 const isLowStock = daysOfStock !== null && daysOfStock <= 14;
1285
1286 const isHighDRR = product.drr !== null && product.drr > 20;
1287
1288 const isOutOfStock = product.stock === 0 || product.stock === null || (daysOfStock !== null && daysOfStock < 2);
1289
1290
1291 const isLowDRR = product.drr !== null && product.drr <= 17;
1292
1293 const isGrowth = this.detectGrowth(product, avgMetrics);
1294
1295 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
1296
1297 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
1298
1299 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
1300
1301 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
1302
1303 (product.profit / product.revenue) < 0.25;
1304
1305
1306
1307 // Проверяем время доставки (парсим число из строки типа "35 ч")
1308
1309 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
1310
1311 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
1312
1313
1314 // Генерируем рекомендации на основе проблем
1315
1316 const recommendations = [];
1317
1318
1319 if (isOutOfStock) {
1320
1321 recommendations.push('Out-of-stock - Срочно поставить товар!');
1322
1323 }
1324
1325
1326
1327 if (isLowStock) {
1328
1329 recommendations.push('Низкие остатки - Поставить товар, повысить цену, снизить ДРР');
1330
1331 }
1332
1333
1334 if (isHighDRR) {
1335
1336 recommendations.push('Высокий ДРР - Понизить ДРР, Снизить цену');
1337
1338 }
1339
1340
1341 if (isLowDRR) {
1342
1343 recommendations.push('Повысить ДРР - Повысить ДРР, Повысить цену');
1344
1345 }
1346
1347
1348 if (isLowImpressions) {
1349
1350 recommendations.push('Упали Показы - Проверить остатки, Повысить ДРР, Понизить цену');
1351
1352 }
1353
1354
1355
1356 if (isLowCR) {
1357
1358 recommendations.push('Упал CR - Проверить остатки, Понизить цену');
1359
1360 }
1361
1362
1363
1364 if (isLowProfit) {
1365
1366 recommendations.push('Низкая прибыль - снизить ДРР, проверить цену');
1367
1368 }
1369
1370
1371
1372 if (isBadDeliveryTime) {
1373
1374 recommendations.push('Плохое время - проверить остатки, сделать поставку');
1375
1376 }
1377
1378
1379 if (isGrowth) {
1380
1381 recommendations.push('Рост - поднять цену');
1382
1383 }
1384
1385
1386
1387 // Если нет проблем - выводим "Всё хорошо"
1388
1389 if (recommendations.length === 0) {
1390
1391 recommendations.push('Всё хорошо, рекомендаций нет');
1392
1393 }
1394
1395
1396
1397 return {
1398
1399 priority: 'low',
1400
1401 problems: [],
1402
1403 recommendations,
1404
1405 daysOfStock,
1406
1407 isLowStock,
1408
1409 isHighDRR,
1410
1411 isLowDRR,
1412
1413 isGrowth,
1414
1415 isLowImpressions,
1416
1417 isLowCR,
1418
1419 isLowProfit,
1420
1421 isBadDeliveryTime,
1422
1423 isOutOfStock
1424
1425 };
1426
1427 }
1428
1429
1430 // Вычисление средних показателей
1431
1432 calculateAverageMetrics(products) {
1433
1434 const validProducts = products.filter(p => p.revenueChange !== null);
1435
1436 if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
1437
1438
1439
1440 const sum = validProducts.reduce((acc, p) => ({
1441
1442 revenueChange: acc.revenueChange + (p.revenueChange || 0),
1443
1444 ordersChange: acc.ordersChange + (p.ordersChange || 0),
1445
1446 impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
1447
1448 }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
1449
1450
1451
1452 return {
1453
1454 revenueChange: sum.revenueChange / validProducts.length,
1455
1456 ordersChange: sum.ordersChange / validProducts.length,
1457
1458 impressionsChange: sum.impressionsChange / validProducts.length
1459
1460 };
1461
1462 }
1463
1464
1465 async analyzeProduct(product, avgMetrics, useAI = true) {
1466
1467 try {
1468
1469 // Используем уже рассчитанное значение daysOfStock из product
1470 const daysOfStock = product.daysOfStock;
1471
1472 const isLowStock = daysOfStock !== null && daysOfStock <= 14;
1473
1474 const isHighDRR = product.drr !== null && product.drr > 20;
1475
1476 const isOutOfStock = product.stock === 0 || product.stock === null || (daysOfStock !== null && daysOfStock < 2);
1477
1478
1479 const isLowDRR = product.drr !== null && product.drr <= 17;
1480
1481 const isGrowth = this.detectGrowth(product, avgMetrics);
1482
1483 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
1484
1485 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
1486
1487 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
1488
1489 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
1490
1491 (product.profit / product.revenue) < 0.25;
1492
1493
1494
1495 // Проверяем время доставки
1496
1497 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
1498
1499 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
1500
1501
1502
1503 if (!useAI) {
1504
1505 return this.basicAnalysis(product, avgMetrics);
1506
1507 }
1508
1509
1510
1511 // Формируем промпт для AI
1512
1513 const prompt = `Ты — AI‑аналитик маркетплейса Ozon. На вход ты получаешь показатели одного товара за выбранный период и их динамику к прошлому периоду.
1514
1515Метрики, которые ты можешь видеть:
1516- выручка, прибыль, маржа в %;
1517- заказы, корзины;
1518- показы, клики/переходы в карточку, CTR, CR, CRL;
1519- рекламные расходы, ДРР;
1520- цена;
1521- остаток в штуках, показатель «хватит на дней»;
1522- время доставки (в часах);
1523- динамика каждого показателя к прошлому периоду в % (например, «-7%»).
1524
1525Других данных (рейтинг, отзывы, акции) у тебя может не быть — не опирайся на них, если они прямо не переданы.
1526
1527БИЗНЕС‑ЦЕЛЬ:
1528Максимизировать оборот (выручку и заказы) при соблюдении ограничений:
1529- целевая маржа — не ниже 25% (нормальный диапазон 25–30%; выше 30% — высокая маржа);
1530- целевой ДРР — около 20% (нормальный диапазон 17–25%).
1531- абсолютный минимум маржи, ниже которого НЕЛЬЗЯ предлагать снижать цену — 15%.
1532
1533Допустимы временные отклонения:
1534- при агрессивном разгоне нового товара можно повышать ДРР до ~35%, если прибыль остаётся положительной и маржа не падает ниже 30%;
1535- при огромных остатках (>60 дней запаса) или выводе товара допускается снижать маржу, но всё равно не ниже 15%.
1536
1537ДОПУСТИМЫЕ РЫЧАГИ УПРАВЛЕНИЯ:
1538Ты можешь рекомендовать действия только в четырёх областях:
15391. Цена.
15402. Реклама / ДРР (ставки, бюджеты).
15413. Остатки и логистика (объём и распределение остатков, поставки, влияние на срок доставки).
15424. Карточка товара (фото, заголовок, описание).
1543
1544Запрещено предлагать любые действия вне этих четырёх областей.
1545
1546ПОРОГИ И ОПРЕДЕЛЕНИЯ СОСТОЯНИЙ:
1547
1548Время доставки:
1549- Нормальное: ≤ 35 часов.
1550- Плохое: > 35 и ≤ 40 часов, особенно если выросло к прошлому периоду.
1551- Критически плохое: > 40 часов.
1552
1553Остатки / «хватит на дней»:
1554- Критически мало: ≤ 7 дней.
1555- Низкий запас: > 7 и ≤ 14 дней.
1556- Нормально: > 14 и ≤ 30 дней.
1557- Избыточный запас: > 30 дней.
1558- Огромные остатки: > 60 дней.
1559
1560Маржа (прибыль в %):
1561- Очень низкая: < 20%.
1562- Низкая: 20–25%.
1563- Нормальная: 25–30%.
1564- Высокая: > 30%.
1565- Абсолютный минимум, ниже которого нельзя снижать цену — 15%.
1566
1567ДРР:
1568- Целевой ДРР: ~20%.
1569- Слишком низкий ДРР: < 17% (есть запас для роста трафика).
1570- Нормальный ДРР: 17–25%.
1571- Слишком высокий ДРР: > 25% (режет прибыль; допустим только как временная мера при разгоне нового товара).
1572
1573ШАГИ ИЗМЕНЕНИЯ ЦЕНЫ:
1574Разовые рекомендации по изменению цены (в любую сторону) должны быть в диапазоне примерно 5–10%, не больше. Нельзя предлагать очень резких скачков цены за один шаг.
1575
1576ПРИОРИТЕТЫ АНАЛИЗА:
1577
15781. Остатки и время доставки — первый приоритет.
15792. Потом — выручка, заказы, прибыль, маржа и ДРР.
15803. Затем — показы, CTR, CR, CRL, конверсия и качество карточки.
1581
1582ПРАВИЛА АНАЛИЗА:
1583
15841. ОСТАТКИ И ВРЕМЯ ДОСТАВКИ — ГЛАВНЫЕ СТОП‑ФАКТОРЫ
1585
1586Если остатки критически низкие или низкие (≤14 дней) ИЛИ время доставки плохое/критическое (>35 часов):
1587
1588- Главная задача — не уйти в out of stock и восстановить нормальную доставку.
1589- Рекомендовать в первую очередь:
1590 • [Остатки] Поставку товара и/или перераспределение на ближайший склад, чтобы довести запас минимум до 14–30 дней и сократить срок доставки.
1591 • [Цена] При необходимости временно ПОВЫСИТЬ цену (на 5–10%), чтобы притормозить продажи, пока товар едет или пока не нормализуется доставка.
1592 • [Реклама] Временно СНИЗИТЬ ДРР (урезать ставки и бюджеты, отключить самые неэффективные кампании/ключи), чтобы не разгонять спрос.
1593
1594ВАЖНО: В этом состоянии СТРОГО ЗАПРЕЩЕНО:
1595- советовать снижение цены,
1596- советовать повышение ДРР,
1597даже если упали показы, клики или заказы.
1598
15992. НОРМАЛЬНЫЕ ОСТАТКИ И ДОСТАВКА
1600
1601Если остатки >14 дней и время доставки ≤35 часов (нормальные):
1602
1603- Можно фокусироваться на росте оборта и оптимизации прибыли в рамках целевых маржи и ДРР.
1604- При избыточных остатках:
1605 • 30–60 дней: можно аккуратно усиливать спрос (повышать ДРР, немного снижать цену, но по возможности держать маржу ≥25% и не опускаться ниже 20%).
1606 • >60 дней (огромные остатки): допусти более агрессивный разгон (снижение цены, повышение ДРР), но маржа всё равно не должна опускаться ниже 15%.
1607
16083. ВЫРУЧКА, ЗАКАЗЫ, ПРИБЫЛЬ, МАРЖА
1609
1610- Если выручка и заказы падают при нормальных остатках и доставке:
1611 • искать причины в показах, рекламе, цене, конверсии и качестве карточки;
1612 • смотреть динамику CTR/CR/CRL.
1613
1614- Если прибыль и маржа низкие (ниже 25%) или падают, особенно на фоне высокого ДРР (>25%):
1615 • основная цель — вернуть маржу и прибыль к целевым уровням;
1616 • рекомендовать снижать ДРР (резать ставки и бюджеты, отключать неэффективные кампании/ключи);
1617 • рассматривать повышение цены (в разумных пределах 5–10%), если спрос это позволяет;
1618 • не предлагать существенное снижение цены, если маржа уже низкая или близка к минимуму (особенно ниже 20%).
1619
16204. РЕКЛАМА И ДРР
1621
1622- Высокий ДРР (>25%):
1623 • реклама неэффективна или слишком агрессивна;
1624 • рекомендуемые действия:
1625 – [Реклама] Снизить ДРР: уменьшить ставки и бюджеты, отключить слабые кампании/запросы;
1626 – при нормальных остатках и низком CR дополнительно: немного снизить цену и/или улучшить карточку, чтобы поднять конверсию;
1627 – исключение — новый товар: временно можно терпеть ДРР до ~35%, но с контролем маржи (не ниже 15–20%).
1628
1629- Слишком низкий ДРР (<17%) при нормальной/высокой марже и достатенных остатках:
1630 • есть потенциал роста оборта;
1631 • рекомендовать:
1632 – [Реклама] Постепенно повышать ДРР: поднимать ставки, расширять кампании;
1633 – при необходимости можно немного поднять цену, чтобы сохранить маржу при росте трафика.
1634
1635- Если показы упали, а остатки и доставка в норме:
1636 • в приоритете рекламные действия:
1637 – [Реклама] Повысить ДРР для ускорения продаж (если ДРР низкий);
1638 • вторым шагом:
1639 – [Цена] Немного снизить цену (5–10%), если маржа выше целевой;
1640 – [Карточка] При необходимости доработать заголовок и ключевые слова для релевантности.
1641
16426. ПОЛОЖИТЕЛЬНАЯ ДИНАМИКА
1643
1644Если выручка, заказы и прибыль растут, маржа ≥25%, ДРР в норме (17–25%), остатки достаточные (>14 дней) и нет риска out of stock, срок доставки хороший (≤35 часов):
1645
1646- не предлагать резких изменений;
1647- основная рекомендация:
1648 • [Цена] Осторожно тестировать повышение цены на 5–10% с контролем CR и выручки;
1649 • [Реклама] При низком ДРР и хорошем запасе по прибыли — мягко расширять рекламу.
1650
16517. НОВЫЕ ТОВАРЫ И ВЫВОД ТОВАРА
1652
1653- Новый товар (распознавать по очень малому количеству заказов за период при нормальных/избыточных остатках):
1654 • допустимо:
1655 – [Реклама] Повышать ДРР до ~35% ради разгона, если есть запас по марже и бюджу;
1656 – [Цена] Сильных скидок не давать сразу, чтобы не убивать маржу; тестировать цену аккуратно.
1657
1658- Товар под вывод / очень большие остатки (запас >60 дней и слабые продажи):
1659 • КРИТИЧЕСКИ ВАЖНО: Перед любой рекомендацией по снижению цены ОБЯЗАТЕЛЬНО проверь текущую маржу:
1660 – Если маржа <20% (низкая или очень низкая): СТРОГО ЗАПРЕЩЕНО рекомендовать снижение цены в любом виде.
1661 * Вместо этого рекомендуй:
1662 [Реклама] Повысить ДРР для ускорения продаж (если ДРР низкий);
1663 [Карточка] Улучшить карточку товара для повышения конверсии;
1664 [Остатки] Рассмотреть перераспределение остатков или вывод товара из ассортимента.
1665 – Если маржа ≥20% и <25%: можно осторожно рекомендовать снижение цены на 3-5%, но с обязательным контролем, чтобы маржа не упала ниже 15%.
1666 – Если маржа ≥25%: можно рекомендовать снижение цены на 5-10%, но следить, чтобы маржа не упала ниже 20%.
1667 • ЗАПРЕЩЕНО: Рекомендовать снижение цены без явной проверки и упоминания текущей маржи в рекомендации.
1668 • допустимо:
1669 – [Реклама] Поддерживать или немного повышать ДРР, если это экономически оправдано;
1670 – цель — ускорить оборачиваемость без ухода в ноль или минус по прибыли.
1671
1672ФОРМАТ ОТВЕТА:
1673
16741. Блок «Диагноз» — 1–3 коротких предложения:
1675 - что происходит с товаром (рост/падение выручки, заказов, прибыли);
1676 - в чём основная причина (остатки, реклама, цена, карточка, доставка, конверсия).
1677
16782. Блок «Рекомендации» — маркированный список из 3–7 пунктов.
1679
1680Каждый пункт рекомендаций должен начинаться с области в квадратных скобках: [Цена] / [Реклама] / [Остатки] / [Карточка], а дальше — конкретное действие и короткое объяснение «зачем».
1681
1682Примеры формата:
1683- [Остатки] Сделать поставку на ближайший склад на 2–3 недели продаж, чтобы сократить срок доставки и не уйти в out of stock.
1684- [Реклама] Снизить ДРР с ~25% до ~18–20%, отключив неэффективные кампании — это повысит маржу и прибыль.
1685- [Цена] Тестово поднять цену на 5–10%, так как показатели растут и есть запас по конверсии.
1686- [Карточка] Переработать первое фото и заголовок, чтобы повысить CTR и вернуть посещаемость карточки.
1687
1688ОГРАНИЧЕНИЯ:
1689- Не перечисляй все возможные варианты действий — выбирай только те, которые реально вытекают из данных и не противоречат друг другу.
1690- Если информации не хватает для однозначного вывода — напиши, какие есть возможные причины и дай для каждой гипотезы отдельный набор действий.
1691- Пиши простым, понятным языком, без сложных терминов и «воды».
1692
1693--------------------
1694ДАННЫЕ ТОВАРА
1695--------------------
1696
1697Товар: ${product.name}
1698Артикул: ${product.article}
1699
1700Метрики:
1701- Выручка: ${product.revenue || 'н/д'} ₽ (изменение: ${product.revenueChange || 0}%)
1702- Прибыль: ${product.profit || 'н/д'} ₽ (${product.profitPercent || 'н/д'}% от выручки)
1703- Показы всего: ${product.impressions || 'н/д'} (изменение: ${product.impressionsChange || 0}%)
1704- Посещения карточки: ${product.cardVisits || 'н/д'} (изменение: ${product.cardVisitsChange || 0}%)
1705- CTR (каталог→карточка): ${product.conversionCatalogToCard || 'н/д'}% (изменение: ${product.conversionCatalogToCardChange || 0}%)
1706- Добавления в корзину: ${product.cartAdditions || 'н/д'} (изменение: ${product.cartAdditionsChange || 0}%)
1707- CRL (карточка→корзина): ${product.conversionCardToCart || 'н/д'}% (изменение: ${product.conversionCardToCartChange || 0}%)
1708- Заказы: ${product.orders || 'н/д'} шт (изменение: ${product.ordersChange || 0}%)
1709- CR (заказы/карточка): ${product.cr || 'н/д'}%
1710- ДРР: ${product.drr || 'н/д'}% (изменение: ${product.drrChange || 0}%)
1711- Средняя цена: ${product.avgPrice || 'н/д'} ₽ (изменение: ${product.avgPriceChange || 0}%)
1712- Остаток: ${product.stock || 'н/д'} шт
1713- На дней: ${daysOfStock || 'н/д'} дней
1714- Доставка: ${product.deliveryTime || 'н/д'}
1715
1716 `;
1717
1718 const response = await RM.aiCall(prompt, {
1719
1720 type: 'json_schema',
1721
1722 json_schema: {
1723
1724 name: 'product_analysis',
1725
1726 schema: {
1727
1728 type: 'object',
1729
1730 properties: {
1731
1732 priority: {
1733
1734 type: 'string',
1735
1736 enum: ['critical', 'high', 'medium', 'low']
1737
1738 },
1739
1740 problems: {
1741
1742 type: 'array',
1743
1744 items: {
1745
1746 type: 'object',
1747
1748 properties: {
1749
1750 type: { type: 'string' },
1751
1752 description: { type: 'string' }
1753
1754 },
1755
1756 required: ['type', 'description']
1757
1758 }
1759
1760 },
1761
1762 recommendations: {
1763
1764 type: 'array',
1765
1766 items: { type: 'string' }
1767
1768 }
1769
1770 },
1771
1772 required: ['priority', 'problems', 'recommendations']
1773
1774 }
1775
1776 }
1777
1778 });
1779
1780
1781 return {
1782
1783 ...response,
1784
1785 daysOfStock,
1786
1787 isLowStock,
1788
1789 isHighDRR,
1790
1791 isLowDRR,
1792
1793 isGrowth,
1794
1795 isLowImpressions,
1796
1797 isLowCR,
1798
1799 isLowProfit,
1800
1801 isBadDeliveryTime,
1802
1803 isOutOfStock
1804
1805 };
1806
1807 } catch (error) {
1808
1809 console.error('Ошибка AI анализа:', error);
1810
1811 return this.basicAnalysis(product, avgMetrics);
1812
1813 }
1814
1815 }
1816
1817
1818 // Определение роста на основе средних показателей
1819
1820 detectGrowth(product, avgMetrics) {
1821
1822 const threshold = 15; // Порог отклонения от среднего в %
1823
1824
1825
1826 // Если выручка растет значительно выше среднего
1827
1828 if (product.revenueChange !== null &&
1829
1830 product.revenueChange > avgMetrics.revenueChange + threshold) {
1831
1832 return true;
1833
1834 }
1835
1836
1837
1838 // Если заказы растут значительно выше среднего
1839
1840 if (product.ordersChange !== null &&
1841
1842 product.ordersChange > avgMetrics.ordersChange + threshold) {
1843
1844 return true;
1845
1846 }
1847
1848
1849
1850 return false;
1851
1852 }
1853
1854 }
1855
1856
1857 // Класс для UI
1858
1859 class AnalyticsUI {
1860
1861 constructor() {
1862
1863 this.container = null;
1864
1865 this.filteredProducts = [];
1866
1867 this.allProducts = [];
1868
1869 this.currentFilter = 'all';
1870
1871 this.isCollapsed = false;
1872
1873 this.isDragging = false;
1874
1875 this.isResizing = false;
1876
1877 this.dragStartX = 0;
1878
1879 this.dragStartY = 0;
1880
1881 this.containerStartX = 0;
1882
1883 this.containerStartY = 0;
1884
1885 this.resizeStartWidth = 0;
1886
1887 this.resizeStartHeight = 0;
1888
1889 }
1890
1891
1892 createUI() {
1893
1894 console.log('🎨 Создаем UI...');
1895
1896
1897 // Создаем контейнер для нашего UI
1898
1899 this.container = document.createElement('div');
1900
1901 this.container.id = 'ozon-ai-analytics';
1902
1903 this.container.style.cssText = `
1904
1905 position: fixed;
1906
1907 top: 80px;
1908
1909 right: 20px;
1910
1911 width: 500px;
1912
1913 max-height: 85vh;
1914
1915 background: white;
1916
1917 border-radius: 12px;
1918
1919 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
1920
1921 z-index: 10000;
1922
1923 overflow: hidden;
1924
1925 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1926
1927 resize: both;
1928
1929 min-width: 400px;
1930
1931 min-height: 200px;
1932
1933 `;
1934
1935
1936 // Заголовок (с возможностью перетаскивания)
1937
1938 const header = document.createElement('div');
1939
1940 header.id = 'ozon-ai-header';
1941
1942 header.style.cssText = `
1943
1944 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1945
1946 color: white;
1947
1948 padding: 18px 24px;
1949
1950 font-weight: 600;
1951
1952 font-size: 18px;
1953
1954 display: flex;
1955
1956 justify-content: space-between;
1957
1958 align-items: center;
1959
1960 cursor: move;
1961
1962 user-select: none;
1963
1964 `;
1965
1966 header.innerHTML = `
1967
1968 <span>🤖 AI Аналитик Продаж</span>
1969
1970 <div style="display: flex; gap: 8px; align-items: center;">
1971
1972 <button id="ozon-ai-collapse" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Свернуть/Развернуть">−</button>
1973
1974 <button id="ozon-ai-close" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Закрыть">×</button>
1975
1976 </div>
1977
1978 `;
1979
1980
1981 // Кнопка запуска анализа
1982
1983 const startButton = document.createElement('button');
1984
1985 startButton.id = 'ozon-ai-start';
1986
1987 startButton.textContent = '🚀 Запустить анализ';
1988
1989 startButton.style.cssText = `
1990
1991 width: calc(100% - 40px);
1992
1993 margin: 20px;
1994
1995 padding: 16px;
1996
1997 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1998
1999 color: white;
2000
2001 border: none;
2002
2003 border-radius: 8px;
2004
2005 font-size: 16px;
2006
2007 font-weight: 600;
2008
2009 cursor: pointer;
2010
2011 transition: transform 0.2s;
2012
2013 `;
2014
2015 startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
2016
2017 startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
2018
2019
2020 // Контейнер для контента
2021
2022 const content = document.createElement('div');
2023
2024 content.id = 'ozon-ai-content';
2025
2026 content.style.cssText = `
2027
2028 padding: 20px;
2029
2030 max-height: calc(85vh - 140px);
2031
2032 overflow-y: auto;
2033
2034 `;
2035
2036
2037 // Индикатор изменения размера
2038
2039 const resizeHandle = document.createElement('div');
2040
2041 resizeHandle.id = 'ozon-ai-resize';
2042
2043 resizeHandle.style.cssText = `
2044
2045 position: absolute;
2046
2047 bottom: 0;
2048
2049 right: 0;
2050
2051 width: 20px;
2052
2053 height: 20px;
2054
2055 cursor: nwse-resize;
2056
2057 background: linear-gradient(135deg, transparent 0%, transparent 50%, #667eea 50%, #667eea 100%);
2058
2059 border-bottom-right-radius: 12px;
2060
2061 `;
2062
2063
2064 this.container.appendChild(header);
2065
2066 this.container.appendChild(startButton);
2067
2068 this.container.appendChild(content);
2069
2070 this.container.appendChild(resizeHandle);
2071
2072
2073 document.body.appendChild(this.container);
2074
2075
2076 // События для перетаскивания
2077
2078 header.addEventListener('mousedown', (e) => this.startDragging(e));
2079
2080 document.addEventListener('mousemove', (e) => this.drag(e));
2081
2082 document.addEventListener('mouseup', () => this.stopDragging());
2083
2084
2085 // События для изменения размера
2086
2087 resizeHandle.addEventListener('mousedown', (e) => this.startResizing(e));
2088
2089
2090 // События кнопок
2091
2092 document.getElementById('ozon-ai-close').addEventListener('click', () => {
2093
2094 this.container.style.display = 'none';
2095
2096 });
2097
2098
2099 document.getElementById('ozon-ai-collapse').addEventListener('click', () => {
2100
2101 this.toggleCollapse();
2102
2103 });
2104
2105
2106 document.getElementById('ozon-ai-start').addEventListener('click', () => {
2107
2108 this.startAnalysis();
2109
2110 });
2111
2112
2113 console.log('✅ UI создан');
2114
2115 }
2116
2117
2118 startDragging(e) {
2119
2120 if (e.target.closest('button')) return; // Не перетаскиваем при клике на кнопки
2121
2122 this.isDragging = true;
2123
2124 this.dragStartX = e.clientX;
2125
2126 this.dragStartY = e.clientY;
2127
2128 const rect = this.container.getBoundingClientRect();
2129
2130 this.containerStartX = rect.left;
2131
2132 this.containerStartY = rect.top;
2133
2134 this.container.style.transition = 'none';
2135
2136 }
2137
2138
2139 drag(e) {
2140
2141 if (this.isDragging) {
2142
2143 const deltaX = e.clientX - this.dragStartX;
2144
2145 const deltaY = e.clientY - this.dragStartY;
2146
2147 this.container.style.left = `${this.containerStartX + deltaX}px`;
2148
2149 this.container.style.top = `${this.containerStartY + deltaY}px`;
2150
2151 this.container.style.right = 'auto';
2152
2153 } else if (this.isResizing) {
2154
2155 const deltaX = e.clientX - this.dragStartX;
2156
2157 const deltaY = e.clientY - this.dragStartY;
2158
2159 const newWidth = Math.max(400, this.resizeStartWidth + deltaX);
2160
2161 const newHeight = Math.max(200, this.resizeStartHeight + deltaY);
2162
2163 this.container.style.width = `${newWidth}px`;
2164
2165 this.container.style.maxHeight = `${newHeight}px`;
2166
2167 }
2168
2169 }
2170
2171
2172 stopDragging() {
2173
2174 this.isDragging = false;
2175
2176 this.isResizing = false;
2177
2178 this.container.style.transition = '';
2179
2180 }
2181
2182
2183 startResizing(e) {
2184
2185 e.stopPropagation();
2186
2187 this.isResizing = true;
2188
2189 this.dragStartX = e.clientX;
2190
2191 this.dragStartY = e.clientY;
2192
2193 this.resizeStartWidth = this.container.offsetWidth;
2194
2195 this.resizeStartHeight = this.container.offsetHeight;
2196
2197 }
2198
2199
2200 toggleCollapse() {
2201
2202 this.isCollapsed = !this.isCollapsed;
2203
2204 const content = document.getElementById('ozon-ai-content');
2205
2206 const startButton = document.getElementById('ozon-ai-start');
2207
2208 const resizeHandle = document.getElementById('ozon-ai-resize');
2209
2210 const collapseButton = document.getElementById('ozon-ai-collapse');
2211
2212
2213
2214 if (this.isCollapsed) {
2215
2216 content.style.display = 'none';
2217
2218 startButton.style.display = 'none';
2219
2220 resizeHandle.style.display = 'none';
2221
2222 collapseButton.textContent = '+';
2223
2224 this.container.style.maxHeight = 'auto';
2225
2226 } else {
2227
2228 content.style.display = 'block';
2229
2230 startButton.style.display = 'block';
2231
2232 resizeHandle.style.display = 'block';
2233
2234 collapseButton.textContent = '−';
2235
2236 this.container.style.maxHeight = '85vh';
2237
2238 }
2239
2240 }
2241
2242
2243 async startAnalysis() {
2244
2245 const content = document.getElementById('ozon-ai-content');
2246
2247 const startButton = document.getElementById('ozon-ai-start');
2248
2249
2250
2251 startButton.disabled = true;
2252
2253 startButton.textContent = '⏳ Загрузка товаров...';
2254
2255
2256 try {
2257
2258 // Шаг 1: Загрузка всех товаров
2259
2260 const collector = new ProductDataCollector();
2261
2262 await collector.loadAllProducts();
2263
2264
2265 startButton.textContent = '📊 Сбор данных...';
2266
2267
2268
2269 // Шаг 2: Сбор данных
2270
2271 const products = collector.collectProductData();
2272
2273
2274 if (products.length === 0) {
2275
2276 content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
2277
2278 startButton.disabled = false;
2279
2280 startButton.textContent = '🚀 Запустить анализ';
2281
2282 return;
2283
2284 }
2285
2286
2287 // Шаг 3: AI анализ с прогрессом
2288
2289 const analyzer = new AIAnalyzer();
2290
2291
2292
2293 const onProgress = (current, total, percentage, remaining) => {
2294
2295 const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
2296
2297 startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
2298
2299 };
2300
2301
2302
2303 const analyzedProducts = await analyzer.analyzeProducts(products, onProgress);
2304
2305
2306 this.allProducts = analyzedProducts;
2307
2308 this.filteredProducts = analyzedProducts;
2309
2310
2311 // Вычисляем общую выручку и прибыль
2312
2313 const totalRevenue = analyzedProducts.reduce((sum, p) => sum + (p.revenue || 0), 0);
2314
2315 const totalProfit = analyzedProducts.reduce((sum, p) => sum + (p.profit || 0), 0);
2316
2317 const totalOrders = analyzedProducts.reduce((sum, p) => sum + (p.orders || 0), 0);
2318
2319
2320
2321 // Вычисляем средний ДРР (взвешенный по выручке)
2322
2323 let totalDrrWeighted = 0;
2324
2325 let totalRevenueForDrr = 0;
2326
2327 analyzedProducts.forEach(p => {
2328
2329 if (p.drr !== null && p.revenue) {
2330
2331 totalDrrWeighted += p.drr * p.revenue;
2332
2333 totalRevenueForDrr += p.revenue;
2334
2335 }
2336
2337 });
2338
2339 const avgDrr = totalRevenueForDrr > 0 ? totalDrrWeighted / totalRevenueForDrr : 0;
2340
2341
2342 // Шаг 4: Отображение результатов
2343
2344 this.displayResults(analyzedProducts, {
2345
2346 totalRevenue,
2347
2348 totalProfit,
2349
2350 totalOrders,
2351
2352 avgDrr
2353
2354 });
2355
2356
2357 startButton.textContent = '🔄 Анализировать снова';
2358
2359 startButton.disabled = false;
2360
2361
2362 } catch (error) {
2363
2364 console.error('Ошибка анализа:', error);
2365
2366 content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
2367
2368 startButton.disabled = false;
2369
2370 startButton.textContent = '🚀 Запустить анализ';
2371
2372 }
2373
2374 }
2375
2376
2377 displayResults(products, totals) {
2378
2379 const content = document.getElementById('ozon-ai-content');
2380
2381
2382
2383 // Блок с общими показателями
2384
2385 const totalSalesBlock = this.createTotalSalesBlock(totals);
2386
2387
2388
2389 // Фильтры
2390
2391 const filters = this.createFilters(products);
2392
2393
2394
2395 // Список товаров
2396
2397 const productsList = this.createProductsList(products);
2398
2399 content.innerHTML = '';
2400
2401 content.appendChild(totalSalesBlock);
2402
2403 content.appendChild(filters);
2404
2405 content.appendChild(productsList);
2406
2407 }
2408
2409
2410 createTotalSalesBlock(totals) {
2411
2412 const block = document.createElement('div');
2413
2414 block.id = 'ozon-ai-total-sales';
2415
2416 block.style.cssText = `
2417
2418 margin-bottom: 20px;
2419
2420 padding: 16px;
2421
2422 background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
2423
2424 border-radius: 8px;
2425
2426 `;
2427
2428
2429
2430 const profitColor = totals.totalProfit >= 0 ? '#27ae60' : '#e74c3c';
2431
2432 const profitPercent = totals.totalRevenue > 0 ? ((totals.totalProfit / totals.totalRevenue) * 100).toFixed(1) : 0;
2433
2434 const profitPercentColor = profitPercent >= 25 ? '#27ae60' : '#e74c3c';
2435
2436
2437
2438 block.innerHTML = `
2439
2440 <div style="font-size: 14px; font-weight: 600; color: #2c3e50; margin-bottom: 12px;">📊 Общие показатели</div>
2441
2442 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
2443
2444 <div style="background: white; padding: 10px; border-radius: 6px;">
2445
2446 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Общая выручка</div>
2447
2448 <div style="font-weight: 600; color: #2c3e50;">${totals.totalRevenue.toLocaleString()} ₽</div>
2449
2450 </div>
2451
2452 <div style="background: white; padding: 10px; border-radius: 6px;">
2453
2454 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Общая прибыль</div>
2455
2456 <div style="font-weight: 600; color: ${profitColor};">${totals.totalProfit.toLocaleString()} ₽ <span style="color: ${profitPercentColor};">(${profitPercent}%)</span></div>
2457
2458 </div>
2459
2460 <div style="background: white; padding: 10px; border-radius: 6px;">
2461
2462 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Всего заказов</div>
2463
2464 <div style="font-weight: 600; color: #2c3e50;">${totals.totalOrders.toLocaleString()}</div>
2465
2466 </div>
2467
2468 <div style="background: white; padding: 10px; border-radius: 6px;">
2469
2470 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Средний ДРР</div>
2471
2472 <div style="font-weight: 600; color: #2c3e50;">${totals.avgDrr.toFixed(1)}%</div>
2473
2474 </div>
2475
2476 </div>
2477
2478 `;
2479
2480
2481
2482 return block;
2483
2484 }
2485
2486
2487 // Фильтры
2488
2489 createFilters(products) {
2490
2491 const filtersContainer = document.createElement('div');
2492
2493 filtersContainer.style.cssText = `
2494
2495 margin-bottom: 20px;
2496
2497 `;
2498
2499
2500 // Поле поиска
2501
2502 const searchContainer = document.createElement('div');
2503
2504 searchContainer.style.cssText = `
2505
2506 margin-bottom: 12px;
2507
2508 `;
2509
2510
2511
2512 const searchInput = document.createElement('input');
2513
2514 searchInput.type = 'text';
2515
2516 searchInput.placeholder = '🔍 Поиск по названию или артикулу...';
2517
2518 searchInput.id = 'ozon-ai-search';
2519
2520 searchInput.style.cssText = `
2521
2522 width: 100%;
2523
2524 padding: 10px 12px;
2525
2526 border: 2px solid #ecf0f1;
2527
2528 border-radius: 6px;
2529
2530 font-size: 14px;
2531
2532 font-family: inherit;
2533
2534 outline: none;
2535
2536 transition: border-color 0.2s;
2537
2538 `;
2539
2540
2541
2542 searchInput.addEventListener('focus', () => {
2543
2544 searchInput.style.borderColor = '#667eea';
2545
2546 });
2547
2548
2549
2550 searchInput.addEventListener('blur', () => {
2551
2552 searchInput.style.borderColor = '#ecf0f1';
2553
2554 });
2555
2556
2557
2558 searchInput.addEventListener('input', (e) => {
2559
2560 this.applySearch(e.target.value);
2561
2562 });
2563
2564
2565
2566 searchContainer.appendChild(searchInput);
2567
2568 filtersContainer.appendChild(searchContainer);
2569
2570
2571 // Кнопки фильтров
2572
2573 const buttonsContainer = document.createElement('div');
2574
2575 buttonsContainer.id = 'ozon-ai-filter-buttons';
2576
2577 buttonsContainer.style.cssText = `
2578
2579 display: flex;
2580
2581 flex-wrap: wrap;
2582
2583 gap: 8px;
2584
2585 `;
2586
2587
2588 // Подсчет товаров по категориям
2589
2590 const critical = products.filter(p => p.analysis.priority === 'critical').length;
2591
2592 const high = products.filter(p => p.analysis.priority === 'high').length;
2593
2594 const outOfStock = products.filter(p => p.analysis.isOutOfStock).length;
2595
2596 const lowStock = products.filter(p => p.analysis.isLowStock).length;
2597
2598 const highDRR = products.filter(p => p.analysis.isHighDRR).length;
2599
2600 const lowDRR = products.filter(p => p.analysis.isLowDRR).length;
2601
2602 const growth = products.filter(p => p.analysis.isGrowth).length;
2603
2604 const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
2605
2606 const lowCR = products.filter(p => p.analysis.isLowCR).length;
2607
2608 const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
2609
2610 const badDeliveryTime = products.filter(p => p.analysis.isBadDeliveryTime).length;
2611
2612
2613 const filterButtons = [
2614
2615 { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
2616
2617 { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
2618
2619 { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
2620
2621 { id: 'outOfStock', label: `🚨 Out of-stock (${outOfStock})`, color: '#c0392b' },
2622
2623 { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
2624
2625 { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
2626
2627 { id: 'lowDRR', label: `📊 Повысить ДРР (${lowDRR})`, color: '#16a085' },
2628
2629 { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
2630
2631 { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
2632
2633 { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
2634
2635 { id: 'badDeliveryTime', label: `⏱️ Плохое время (${badDeliveryTime})`, color: '#8e44ad' },
2636
2637 { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
2638
2639 ];
2640
2641
2642 filterButtons.forEach(filter => {
2643
2644 const btn = document.createElement('button');
2645
2646 btn.textContent = filter.label;
2647
2648 btn.dataset.filterId = filter.id;
2649
2650 btn.style.cssText = `
2651
2652 padding: 8px 12px;
2653
2654 background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
2655
2656 color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
2657
2658 border: none;
2659
2660 border-radius: 6px;
2661
2662 font-size: 13px;
2663
2664 font-weight: 500;
2665
2666 cursor: pointer;
2667
2668 transition: all 0.2s;
2669
2670 `;
2671
2672
2673
2674 btn.addEventListener('click', () => {
2675
2676 this.currentFilter = filter.id;
2677
2678 this.applyFilter(filter.id);
2679
2680 });
2681
2682
2683 buttonsContainer.appendChild(btn);
2684
2685 });
2686
2687
2688 filtersContainer.appendChild(buttonsContainer);
2689
2690 return filtersContainer;
2691
2692 }
2693
2694
2695 applySearch(searchTerm) {
2696
2697 const term = searchTerm.toLowerCase().trim();
2698
2699
2700
2701 console.log(`🔍 Поиск по запросу: "${term}"`);
2702
2703
2704
2705 if (!term) {
2706
2707 // Если поиск пустой, применяем текущий фильтр
2708
2709 this.applyFilter(this.currentFilter);
2710
2711 return;
2712
2713 }
2714
2715
2716
2717 // Фильтруем по поисковому запросу
2718
2719 const filtered = this.allProducts.filter(p => {
2720
2721 const nameMatch = p.name.toLowerCase().includes(term);
2722
2723 const articleMatch = p.article.includes(term);
2724
2725 return nameMatch || articleMatch;
2726
2727 });
2728
2729
2730
2731 console.log(`✅ Найдено товаров: ${filtered.length}`);
2732
2733
2734
2735 // Сортируем по выручке
2736
2737 filtered.sort((a, b) => {
2738
2739 const revenueA = a.revenue || 0;
2740
2741 const revenueB = b.revenue || 0;
2742
2743 return revenueB - revenueA;
2744
2745 });
2746
2747
2748 this.filteredProducts = filtered;
2749
2750
2751
2752 // Обновляем только список товаров, не трогая фильтры
2753
2754 const content = document.getElementById('ozon-ai-content');
2755
2756 const productsList = this.createProductsList(filtered);
2757
2758
2759
2760 // Находим и удаляем только список товаров (третий элемент в content)
2761
2762 const children = content.children;
2763
2764 if (children.length > 2) {
2765
2766 children[2].remove();
2767
2768 }
2769
2770
2771
2772 content.appendChild(productsList);
2773
2774 }
2775
2776
2777 applyFilter(filterId) {
2778
2779 let filtered = this.allProducts;
2780
2781
2782
2783 console.log(`🔍 Применяем фильтр: ${filterId}`);
2784
2785
2786
2787 // Очищаем поле поиска при смене фильтра
2788
2789 const searchInput = document.getElementById('ozon-ai-search');
2790
2791 if (searchInput) {
2792
2793 searchInput.value = '';
2794
2795 }
2796
2797
2798 switch(filterId) {
2799
2800 case 'critical':
2801
2802 filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
2803
2804 break;
2805
2806 case 'high':
2807
2808 filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
2809
2810 break;
2811
2812 case 'outOfStock':
2813
2814 filtered = this.allProducts.filter(p => p.analysis.isOutOfStock);
2815
2816 break;
2817
2818 case 'lowStock':
2819
2820 filtered = this.allProducts.filter(p => p.analysis.isLowStock);
2821
2822 break;
2823
2824 case 'highDRR':
2825
2826 filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
2827
2828 break;
2829
2830 case 'lowDRR':
2831
2832 filtered = this.allProducts.filter(p => p.analysis.isLowDRR);
2833
2834 break;
2835
2836 case 'lowImpressions':
2837
2838 filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
2839
2840 console.log('📊 Товары с упавшими показами:', filtered.map(p => `${p.article} (${p.impressionsChange}%)`));
2841
2842 break;
2843
2844 case 'lowCR':
2845
2846 filtered = this.allProducts.filter(p => p.analysis.isLowCR);
2847
2848 break;
2849
2850 case 'lowProfit':
2851
2852 filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
2853
2854 break;
2855
2856 case 'badDeliveryTime':
2857
2858 filtered = this.allProducts.filter(p => p.analysis.isBadDeliveryTime);
2859
2860 break;
2861
2862 case 'growth':
2863
2864 filtered = this.allProducts.filter(p => p.analysis.isGrowth);
2865
2866 break;
2867
2868 }
2869
2870
2871 console.log(`✅ Найдено товаров: ${filtered.length}`);
2872
2873
2874 // Сортируем по выручке (от большей к меньшей)
2875
2876 filtered.sort((a, b) => {
2877
2878 const revenueA = a.revenue || 0;
2879
2880 const revenueB = b.revenue || 0;
2881
2882 return revenueB - revenueA;
2883
2884 });
2885
2886
2887 this.filteredProducts = filtered;
2888
2889
2890
2891 // Обновляем только список товаров и кнопки фильтров
2892
2893 const content = document.getElementById('ozon-ai-content');
2894
2895 const productsList = this.createProductsList(filtered);
2896
2897
2898
2899 // Находим и удаляем только список товаров (третий элемент в content)
2900
2901 const children = content.children;
2902
2903 if (children.length > 2) {
2904
2905 children[2].remove();
2906
2907 }
2908
2909
2910
2911 content.appendChild(productsList);
2912
2913
2914
2915 // Обновляем стили кнопок фильтров
2916
2917 this.updateFilterButtons(filterId);
2918
2919 }
2920
2921
2922 updateFilterButtons(activeFilterId) {
2923
2924 const buttonsContainer = document.getElementById('ozon-ai-filter-buttons');
2925
2926 if (!buttonsContainer) return;
2927
2928
2929
2930 const buttons = buttonsContainer.querySelectorAll('button');
2931
2932
2933
2934 const filterColors = {
2935
2936 'all': '#95a5a6',
2937
2938 'critical': '#e74c3c',
2939
2940 'high': '#f39c12',
2941
2942 'outOfStock': '#c0392b',
2943
2944 'lowStock': '#e67e22',
2945
2946 'highDRR': '#c0392b',
2947
2948 'lowDRR': '#16a085',
2949
2950 'lowImpressions': '#9b59b6',
2951
2952 'lowCR': '#e91e63',
2953
2954 'lowProfit': '#d32f2f',
2955
2956 'badDeliveryTime': '#8e44ad',
2957
2958 'growth': '#27ae60'
2959
2960 };
2961
2962
2963
2964 buttons.forEach(btn => {
2965
2966 const filterId = btn.dataset.filterId;
2967
2968 if (filterId === activeFilterId) {
2969
2970 btn.style.background = filterColors[filterId];
2971
2972 btn.style.color = 'white';
2973
2974 } else {
2975
2976 btn.style.background = '#ecf0f1';
2977
2978 btn.style.color = '#2c3e50';
2979
2980 }
2981
2982 });
2983
2984 }
2985
2986
2987 createProductsList(products) {
2988
2989 const list = document.createElement('div');
2990
2991 list.style.cssText = `
2992
2993 display: flex;
2994
2995 flex-direction: column;
2996
2997 gap: 12px;
2998
2999 `;
3000
3001
3002 products.forEach(product => {
3003
3004 const card = this.createProductCard(product);
3005
3006 list.appendChild(card);
3007
3008 });
3009
3010
3011 return list;
3012
3013 }
3014
3015
3016 formatMetric(value, change, isPercent = false) {
3017
3018 // Округляем проценты до десятых
3019
3020 let displayValue = value;
3021
3022 if (isPercent && value !== null && value !== undefined) {
3023
3024 displayValue = parseFloat(value.toFixed(1));
3025
3026 }
3027
3028
3029
3030 const valueStr = displayValue !== null && displayValue !== undefined ?
3031
3032 (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
3033
3034
3035
3036 if (change === null || change === undefined) return valueStr;
3037
3038
3039
3040 // Округляем изменение до десятых
3041
3042 const roundedChange = parseFloat(change.toFixed(1));
3043
3044 const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
3045
3046 const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
3047
3048
3049
3050 return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
3051
3052 }
3053
3054
3055 createProductCard(product) {
3056
3057 const card = document.createElement('div');
3058
3059
3060
3061 const priorityColors = {
3062
3063 critical: '#e74c3c',
3064
3065 high: '#f39c12',
3066
3067 medium: '#3498db',
3068
3069 low: '#95a5a6'
3070
3071 };
3072
3073
3074 const priorityLabels = {
3075
3076 critical: '🔴 Критичный',
3077
3078 high: '🟠 Высокий',
3079
3080 medium: '🟡 Средний',
3081
3082 low: '🟢 Низкий'
3083
3084 };
3085
3086
3087 // Определяем цвет прибыли
3088
3089 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
3090
3091
3092 card.style.cssText = `
3093
3094 background: white;
3095
3096 border: 2px solid ${priorityColors[product.analysis.priority]};
3097
3098 border-radius: 8px;
3099
3100 padding: 14px;
3101
3102 cursor: pointer;
3103
3104 transition: all 0.2s;
3105
3106 `;
3107
3108
3109 card.innerHTML = `
3110
3111 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
3112
3113 <div style="flex: 1;">
3114
3115 <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
3116
3117 <div class="article-copy" style="font-size: 12px; color: #7f8c8d; cursor: pointer; user-select: none;" title="Нажмите, чтобы скопировать артикул">Арт. ${product.article}</div>
3118
3119 </div>
3120
3121 <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
3122
3123 ${priorityLabels[product.analysis.priority]}
3124
3125 </div>
3126
3127 </div>
3128
3129
3130 ${product.analysis.problems.length > 0 ? `
3131
3132 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 13px; font-weight: 500; color: #856404;">
3133
3134 ${product.analysis.problems.slice(0, 2).map(p => `
3135
3136 <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
3137
3138 `).join('')}
3139
3140 </div>
3141
3142 ` : ''}
3143
3144
3145 <!-- БЛОК 1: Выручка / ДРР / Прибыль / Прибыль % / Комиссия / Себестоимость -->
3146 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3147
3148 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">💰 Финансы</div>
3149
3150 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3151
3152 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
3153
3154 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
3155
3156 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</span></div>
3157
3158 <div><strong>Прибыль %:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</span></div>
3159
3160 <div><strong>Комиссия:</strong> ${product.totalCommission !== null ? `${product.totalCommission.toLocaleString()} ₽` : '—'} ${product.totalCommissionPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCommissionPercent}%)</span>` : ''}</div>
3161
3162 <div><strong>Себестоимость:</strong> ${product.totalCost !== null ? `${product.totalCost.toLocaleString()} ₽` : '—'} ${product.totalCostPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCostPercent}%)</span>` : ''}</div>
3163
3164 </div>
3165
3166 </div>
3167
3168
3169 <!-- БЛОК 2: Показы / Посещения карточки / CTR -->
3170 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3171
3172 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">👁️ Трафик</div>
3173
3174 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; font-size: 11px;">
3175
3176 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
3177
3178 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
3179
3180 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
3181
3182 </div>
3183
3184 </div>
3185
3186
3187 <!-- БЛОК 3: Добавления в корзину / CRL / Заказы / CR -->
3188 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3189
3190 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">🛒 Конверсия</div>
3191
3192 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3193
3194 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
3195
3196 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
3197
3198 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
3199
3200 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
3201
3202 </div>
3203
3204 </div>
3205
3206
3207 <!-- БЛОК 4: Остаток / На дней / Время доставки / Цена -->
3208 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px;">
3209
3210 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">📦 Логистика и цена</div>
3211
3212 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3213
3214 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
3215
3216 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
3217
3218 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
3219
3220 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
3221
3222 </div>
3223
3224 </div>
3225
3226
3227 <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
3228
3229 <strong>Рекомендации:</strong>
3230
3231 ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
3232
3233 </div>
3234
3235 `;
3236
3237
3238 // Обработчик копирования артикула
3239
3240 const articleElement = card.querySelector('.article-copy');
3241
3242 articleElement.addEventListener('click', async (e) => {
3243
3244 e.stopPropagation();
3245
3246 try {
3247
3248 await GM.setClipboard(product.article);
3249
3250 const originalText = articleElement.textContent;
3251
3252 articleElement.textContent = '✓ Скопировано!';
3253
3254 articleElement.style.color = '#27ae60';
3255
3256 setTimeout(() => {
3257
3258 articleElement.textContent = originalText;
3259
3260 articleElement.style.color = '#7f8c8d';
3261
3262 }, 1500);
3263
3264 } catch (error) {
3265
3266 console.error('Ошибка копирования:', error);
3267
3268 }
3269
3270 });
3271
3272
3273 card.addEventListener('click', () => {
3274
3275 this.showProductDetails(product);
3276
3277 });
3278
3279
3280 return card;
3281
3282 }
3283
3284
3285 showProductDetails(product) {
3286
3287 // Определяем цвет прибыли
3288
3289 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
3290
3291
3292 // Создаем модальное окно с детальной информацией
3293
3294 const modal = document.createElement('div');
3295
3296 modal.style.cssText = `
3297
3298 position: fixed;
3299
3300 top: 0;
3301
3302 left: 0;
3303
3304 right: 0;
3305
3306 bottom: 0;
3307
3308 background: rgba(0,0,0,0.7);
3309
3310 z-index: 10001;
3311
3312 display: flex;
3313
3314 align-items: center;
3315
3316 justify-content: center;
3317
3318 padding: 20px;
3319
3320 `;
3321
3322
3323 const modalContent = document.createElement('div');
3324
3325 modalContent.style.cssText = `
3326
3327 background: white;
3328
3329 border-radius: 12px;
3330
3331 padding: 24px;
3332
3333 max-width: 700px;
3334
3335 max-height: 80vh;
3336
3337 overflow-y: auto;
3338
3339 width: 100%;
3340
3341 `;
3342
3343
3344 modalContent.innerHTML = `
3345
3346 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
3347
3348 <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
3349
3350 <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
3351
3352 </div>
3353
3354
3355 <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 16px;">Артикул: ${product.article}</div>
3356
3357
3358 <!-- БЛОК 1: Выручка / ДРР / Прибыль / Прибыль % / Комиссия / Себестоимость -->
3359 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3360
3361 💰 Финансы
3362
3363 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3364
3365 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
3366
3367 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
3368
3369 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</span></div>
3370
3371 <div><strong>Прибыль %:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</span></div>
3372
3373 <div><strong>Комиссия:</strong> ${product.totalCommission !== null ? `${product.totalCommission.toLocaleString()} ₽` : '—'} ${product.totalCommissionPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCommissionPercent}%)</span>` : ''}</div>
3374
3375 <div><strong>Себестоимость:</strong> ${product.totalCost !== null ? `${product.totalCost.toLocaleString()} ₽` : '—'} ${product.totalCostPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCostPercent}%)</span>` : ''}</div>
3376
3377 </div>
3378
3379 </div>
3380
3381
3382 <!-- БЛОК 2: Показы / Посещения карточки / CTR -->
3383 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3384
3385 👁️ Трафик
3386
3387 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 13px;">
3388
3389 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
3390
3391 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
3392
3393 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
3394
3395 </div>
3396
3397 </div>
3398
3399
3400 <!-- БЛОК 3: Добавления в корзину / CRL / Заказы / CR -->
3401 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3402
3403 🛒 Конверсия
3404
3405 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3406
3407 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
3408
3409 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
3410
3411 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
3412
3413 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
3414
3415 </div>
3416
3417 </div>
3418
3419
3420 <!-- БЛОК 4: Остаток / На дней / Время доставки / Цена -->
3421 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3422
3423 📦 Логистика и цена
3424
3425 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3426
3427 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
3428
3429 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
3430
3431 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
3432
3433 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
3434
3435 </div>
3436
3437 </div>
3438
3439
3440 <div style="margin-bottom: 16px;">
3441
3442 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
3443
3444 ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
3445
3446 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
3447
3448 <strong>${p.type}:</strong> ${p.description}
3449
3450 </div>
3451
3452 `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
3453
3454 </div>
3455
3456
3457 <div>
3458
3459 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
3460
3461 ${product.analysis.recommendations.map(r => `
3462
3463 <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
3464
3465 ${r}
3466
3467 </div>
3468
3469 `).join('')}
3470
3471 </div>
3472
3473
3474 <button id="filter-by-article" style="
3475
3476 width: 100%;
3477
3478 margin-top: 16px;
3479
3480 padding: 12px;
3481
3482 background: #667eea;
3483
3484 color: white;
3485
3486 border: none;
3487
3488 border-radius: 8px;
3489
3490 font-size: 14px;
3491
3492 font-weight: 600;
3493
3494 cursor: pointer;
3495
3496 ">
3497
3498 🔍 Показать только этот товар в таблице
3499
3500 </button>
3501
3502 `;
3503
3504
3505 modal.appendChild(modalContent);
3506
3507 document.body.appendChild(modal);
3508
3509
3510 // Закрытие модального окна
3511
3512 modal.addEventListener('click', (e) => {
3513
3514 if (e.target === modal) {
3515
3516 modal.remove();
3517
3518 }
3519
3520 });
3521
3522
3523 modalContent.querySelector('#close-modal').addEventListener('click', () => {
3524
3525 modal.remove();
3526
3527 });
3528
3529
3530 // Фильтрация по артикулу
3531
3532 modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
3533
3534 this.filterByArticle(product.article);
3535
3536 modal.remove();
3537
3538 });
3539
3540 }
3541
3542
3543 filterByArticle(article) {
3544
3545 console.log(`🔍 Фильтруем по артикулу: ${article}`);
3546
3547
3548
3549 // Находим поле фильтра по артикулу на странице
3550
3551 const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
3552
3553
3554
3555 if (articleInput) {
3556
3557 articleInput.value = article;
3558
3559 articleInput.dispatchEvent(new Event('input', { bubbles: true }));
3560
3561 articleInput.dispatchEvent(new Event('change', { bubbles: true }));
3562
3563
3564
3565 // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
3566
3567 const buttons = document.querySelectorAll('button[type="submit"]');
3568
3569 let applyButton = null;
3570
3571 for (const btn of buttons) {
3572
3573 if (btn.textContent.includes('Применить')) {
3574
3575 applyButton = btn;
3576
3577 break;
3578
3579 }
3580
3581 }
3582
3583
3584
3585 if (applyButton) {
3586
3587 setTimeout(() => applyButton.click(), 300);
3588
3589 }
3590
3591 } else {
3592
3593 console.warn('Не найдено поле для ввода артикула');
3594
3595 }
3596
3597 }
3598
3599 }
3600
3601
3602 // Инициализация
3603
3604 async function init() {
3605
3606 console.log('🎯 Инициализация AI Аналитика Продаж...');
3607
3608
3609 // Проверяем, что мы на странице аналитики
3610
3611 if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
3612
3613 console.log('⚠️ Не на странице аналитики, ожидаем...');
3614
3615 return;
3616
3617 }
3618
3619
3620 // Ждем загрузки таблицы
3621
3622 const waitForTable = setInterval(() => {
3623
3624 const table = document.querySelector('table.ct590-a');
3625
3626 if (table) {
3627
3628 clearInterval(waitForTable);
3629
3630 console.log('✅ Таблица найдена, создаем UI');
3631
3632 const ui = new AnalyticsUI();
3633
3634 ui.createUI();
3635
3636 }
3637
3638 }, 1000);
3639
3640 }
3641
3642
3643 // Запуск при загрузке страницы
3644
3645 if (document.readyState === 'loading') {
3646
3647 document.addEventListener('DOMContentLoaded', init);
3648
3649 } else {
3650
3651 init();
3652
3653 }
3654
3655
3656 // Отслеживание изменений URL (для SPA)
3657
3658 let lastUrl = location.href;
3659
3660 new MutationObserver(() => {
3661
3662 const url = location.href;
3663
3664 if (url !== lastUrl) {
3665
3666 lastUrl = url;
3667
3668 init();
3669
3670 }
3671
3672 }).observe(document, { subtree: true, childList: true });
3673
3674
3675})();