Ozon AI Analyzer 2.1

Мощный 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})();
Ozon AI Analyzer 2.1 | Robomonkey