Ozon AI Analyzer 5.0

Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению

Size

134.7 KB

Version

1.1.119

Created

Mar 24, 2026

Updated

23 days ago

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