Ozon AI Analyzer 2.0

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

Size

83.0 KB

Version

1.1.26

Created

Dec 5, 2025

Updated

7 days ago

1// ==UserScript==
2// @name		Ozon AI Analyzer 2.0
3// @description		Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version		1.1.26
5// @match		https://*.seller.ozon.ru/*
6// @icon		https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.xmlHttpRequest
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    console.log('🚀 Ozon AI Аналитик Продаж запущен');
15
16    // Утилита для задержки
17    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
18
19    // Парсинг числовых значений
20    function parseNumber(str) {
21        if (!str || str === '—' || str === '') return null;
22        // Извлекаем первое число ДО знака процента
23        const match = str.match(/^([\d\s,.]+)/);
24        if (!match) return null;
25        // Убираем пробелы (разделители тысяч), потом парсим
26        const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
27        const num = parseFloat(cleaned);
28        return isNaN(num) ? null : num;
29    }
30
31    // Парсинг процентов
32    function parsePercent(str) {
33        if (!str || str === '—' || str === '') return null;
34        const match = str.match(/([+-]?\d+(?:\.\d+)?)\s*%/);
35        return match ? parseFloat(match[1]) : null;
36    }
37
38    // Парсинг цены (убираем пробелы между цифрами)
39    function parsePrice(str) {
40        if (!str || str === '—' || str === '') return null;
41        // Извлекаем первое число ДО знака процента
42        const match = str.match(/^([\d\s,.]+)/);
43        if (!match) return null;
44        // Убираем пробелы, затем все кроме цифр и точек
45        const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
46        const num = parseFloat(cleaned);
47        return isNaN(num) ? null : num;
48    }
49
50    // Таблица с данными для расчета прибыли
51    const PRODUCT_COST_DATA = {
52        '72252': { cost: 158.4, commission: 0.27, delivery: 90 },
53        '71613': { cost: 108, commission: 0.27, delivery: 90 },
54        '73716': { cost: 126, commission: 0.27, delivery: 90 },
55        '80036': { cost: 103.2, commission: 0.27, delivery: 90 },
56        '73365': { cost: 166.8, commission: 0.27, delivery: 90 },
57        '74881': { cost: 135.6, commission: 0.27, delivery: 90 },
58        '73266': { cost: 708, commission: 0.27, delivery: 90 },
59        '73655': { cost: 219.6, commission: 0.27, delivery: 90 },
60        '75222': { cost: 103.2, commission: 0.27, delivery: 90 },
61        '73358': { cost: 163.2, commission: 0.27, delivery: 90 },
62        '73723': { cost: 116.4, commission: 0.27, delivery: 90 },
63        '74119': { cost: 110.4, commission: 0.27, delivery: 90 },
64        '72573': { cost: 146.4, commission: 0.27, delivery: 90 },
65        '72221': { cost: 90, commission: 0.27, delivery: 90 },
66        '75345': { cost: 111.6, commission: 0.27, delivery: 90 },
67        '73334': { cost: 104.4, commission: 0.27, delivery: 90 },
68        '72184': { cost: 177.6, commission: 0.27, delivery: 90 },
69        '75291': { cost: 100.8, commission: 0.27, delivery: 90 },
70        '73617': { cost: 163.2, commission: 0.27, delivery: 90 },
71        '73976': { cost: 170.4, commission: 0.27, delivery: 90 },
72        '75710': { cost: 172.8, commission: 0.27, delivery: 90 },
73        '76113': { cost: 115.2, commission: 0.27, delivery: 90 },
74        '75499': { cost: 96, commission: 0.27, delivery: 90 },
75        '71569': { cost: 117.6, commission: 0.27, delivery: 90 },
76        '72276': { cost: 286.8, commission: 0.27, delivery: 90 },
77        '75338': { cost: 90, commission: 0.27, delivery: 90 },
78        '75536': { cost: 87.6, commission: 0.27, delivery: 90 },
79        '76014': { cost: 135.6, commission: 0.27, delivery: 90 },
80        '73730': { cost: 121.2, commission: 0.27, delivery: 90 },
81        '75628': { cost: 124.8, commission: 0.27, delivery: 90 },
82        '74249': { cost: 312, commission: 0.27, delivery: 90 },
83        '75468': { cost: 138, commission: 0.27, delivery: 90 },
84        '73495': { cost: 120, commission: 0.27, delivery: 90 },
85        '74393': { cost: 126, commission: 0.27, delivery: 90 },
86        '74188': { cost: 174, commission: 0.27, delivery: 90 },
87        '73907': { cost: 155.376, commission: 0.27, delivery: 90 },
88        '73396': { cost: 112.8, commission: 0.27, delivery: 90 },
89        '71668': { cost: 96, commission: 0.27, delivery: 90 },
90        '73235': { cost: 114, commission: 0.27, delivery: 90 },
91        '75093': { cost: 96, commission: 0.27, delivery: 90 },
92        '73891': { cost: 100.8, commission: 0.27, delivery: 90 },
93        '75505': { cost: 91.2, commission: 0.27, delivery: 90 },
94        '71590': { cost: 100.8, commission: 0.27, delivery: 90 },
95        '73488': { cost: 150, commission: 0.27, delivery: 90 },
96        '75413': { cost: 128.4, commission: 0.27, delivery: 90 },
97        '76403': { cost: 352.8, commission: 0.27, delivery: 90 },
98        '74799': { cost: 162, commission: 0.27, delivery: 90 },
99        '75406': { cost: 117.6, commission: 0.27, delivery: 90 },
100        '75154': { cost: 123.6, commission: 0.27, delivery: 90 },
101        '75383': { cost: 82.8, commission: 0.27, delivery: 90 },
102        '80029': { cost: 67.416, commission: 0.27, delivery: 90 },
103        '76120': { cost: 264, commission: 0.27, delivery: 90 },
104        '72306': { cost: 186, commission: 0.27, delivery: 90 },
105        '75246': { cost: 88.8, commission: 0.27, delivery: 90 },
106        '73228': { cost: 133.476, commission: 0.27, delivery: 90 },
107        '73419': { cost: 165.6, commission: 0.27, delivery: 90 },
108        '74379': { cost: 175.2, commission: 0.27, delivery: 90 },
109        '83356': { cost: 229.2, commission: 0.27, delivery: 90 },
110        '75444': { cost: 123.6, commission: 0.27, delivery: 90 },
111        '79992': { cost: 127.2, commission: 0.27, delivery: 90 },
112        '73709': { cost: 218.4, commission: 0.27, delivery: 90 },
113        '73778': { cost: 144, commission: 0.27, delivery: 90 },
114        '72269': { cost: 194.4, commission: 0.27, delivery: 90 },
115        '73440': { cost: 118.8, commission: 0.27, delivery: 90 },
116        '74669': { cost: 176.4, commission: 0.27, delivery: 90 },
117        '77660': { cost: 0, commission: 0.27, delivery: 90 },
118        '77578': { cost: 0, commission: 0.27, delivery: 90 },
119        '71545': { cost: 124.8, commission: 0.27, delivery: 90 },
120        '75673': { cost: 97.2, commission: 0.27, delivery: 90 },
121        '76168': { cost: 80.4, commission: 0.27, delivery: 90 },
122        '75277': { cost: 217.2, commission: 0.27, delivery: 90 },
123        '75390': { cost: 108, commission: 0.27, delivery: 90 },
124        '74263': { cost: 160.8, commission: 0.27, delivery: 90 },
125        '74676': { cost: 176.4, commission: 0.27, delivery: 90 },
126        '75727': { cost: 121.2, commission: 0.27, delivery: 90 },
127        '74126': { cost: 111.6, commission: 0.27, delivery: 90 },
128        '74294': { cost: 145.2, commission: 0.27, delivery: 90 },
129        '76069': { cost: 220.8, commission: 0.27, delivery: 90 },
130        '71361': { cost: 204, commission: 0.27, delivery: 90 },
131        '73501': { cost: 114, commission: 0.27, delivery: 90 },
132        '72238': { cost: 102, commission: 0.27, delivery: 90 },
133        '75482': { cost: 218.4, commission: 0.27, delivery: 90 },
134        '76489': { cost: 216, commission: 0.27, delivery: 90 },
135        '76076': { cost: 103.2, commission: 0.27, delivery: 90 },
136        '75437': { cost: 69.6, commission: 0.27, delivery: 90 },
137        '75352': { cost: 72, commission: 0.27, delivery: 90 },
138        '75550': { cost: 112.8, commission: 0.27, delivery: 90 },
139        '75529': { cost: 114, commission: 0.27, delivery: 90 },
140        '76021': { cost: 243.6, commission: 0.27, delivery: 90 },
141        '73969': { cost: 91.2, commission: 0.27, delivery: 90 },
142        '73242': { cost: 133.488, commission: 0.27, delivery: 90 },
143        '80111': { cost: 58.8, commission: 0.27, delivery: 90 },
144        '73693': { cost: 159.6, commission: 0.27, delivery: 90 },
145        '75703': { cost: 208.8, commission: 0.27, delivery: 90 },
146        '74980': { cost: 158.4, commission: 0.27, delivery: 90 },
147        '76380': { cost: 145.6, commission: 0.27, delivery: 90 },
148        '77677': { cost: 129.948, commission: 0.27, delivery: 90 },
149        '75369': { cost: 103.2, commission: 0.27, delivery: 90 },
150        '74713': { cost: 180, commission: 0.27, delivery: 90 },
151        '75024': { cost: 114, commission: 0.27, delivery: 90 },
152        '77615': { cost: 0, commission: 0.27, delivery: 90 },
153        '73389': { cost: 136.8, commission: 0.27, delivery: 90 },
154        '74850': { cost: 169.2, commission: 0.27, delivery: 90 },
155        '75192': { cost: 180, commission: 0.27, delivery: 90 },
156        '73310': { cost: 140.4, commission: 0.27, delivery: 90 },
157        '73280': { cost: 81.6, commission: 0.27, delivery: 90 },
158        '75048': { cost: 122.4, commission: 0.27, delivery: 90 },
159        '74874': { cost: 140.4, commission: 0.27, delivery: 90 },
160        '71675': { cost: 105.6, commission: 0.27, delivery: 90 },
161        '74225': { cost: 153.6, commission: 0.27, delivery: 90 },
162        '74768': { cost: 117.6, commission: 0.27, delivery: 90 },
163        '73136': { cost: 163.2, commission: 0.27, delivery: 90 },
164        '74300': { cost: 134.4, commission: 0.27, delivery: 90 },
165        '76410': { cost: 328.8, commission: 0.27, delivery: 90 },
166        '74898': { cost: 139.2, commission: 0.27, delivery: 90 },
167        '73129': { cost: 159.6, commission: 0.27, delivery: 90 },
168        '75253': { cost: 117.6, commission: 0.27, delivery: 90 },
169        '75666': { cost: 92.4, commission: 0.27, delivery: 90 },
170        '73839': { cost: 112.8, commission: 0.27, delivery: 90 },
171        '75475': { cost: 115.2, commission: 0.27, delivery: 90 },
172        '76397': { cost: 0, commission: 0.27, delivery: 90 },
173        '76083': { cost: 103.2, commission: 0.27, delivery: 90 },
174        '72207': { cost: 123.6, commission: 0.27, delivery: 90 },
175        '76151': { cost: 340.8, commission: 0.27, delivery: 90 },
176        '74911': { cost: 127.2, commission: 0.27, delivery: 90 },
177        '74775': { cost: 141.6, commission: 0.27, delivery: 90 },
178        '74027': { cost: 182.4, commission: 0.27, delivery: 90 },
179        '72245': { cost: 94.8, commission: 0.27, delivery: 90 },
180        '71705': { cost: 112.8, commission: 0.27, delivery: 90 },
181        '75109': { cost: 124.8, commission: 0.27, delivery: 90 },
182        '75260': { cost: 144, commission: 0.27, delivery: 90 },
183        '74584': { cost: 141.6, commission: 0.27, delivery: 90 },
184        '74331': { cost: 128.4, commission: 0.27, delivery: 90 },
185        '75307': { cost: 224.4, commission: 0.27, delivery: 90 },
186        '72542': { cost: 104.4, commission: 0.27, delivery: 90 },
187        '75642': { cost: 144.54, commission: 0.27, delivery: 90 },
188        '75512': { cost: 88.8, commission: 0.27, delivery: 90 },
189        '70999': { cost: 164.4, commission: 0.27, delivery: 90 },
190        '76137': { cost: 103.788, commission: 0.27, delivery: 90 },
191        '74072': { cost: 148.8, commission: 0.27, delivery: 90 },
192        '73297': { cost: 85.2, commission: 0.27, delivery: 90 },
193        '76465': { cost: 301.452, commission: 0.27, delivery: 90 },
194        '71835': { cost: 73.2, commission: 0.27, delivery: 90 },
195        '74324': { cost: 129.6, commission: 0.27, delivery: 90 },
196        '71644': { cost: 132, commission: 0.27, delivery: 90 },
197        '75420': { cost: 106.8, commission: 0.27, delivery: 90 },
198        '74355': { cost: 182.4, commission: 0.27, delivery: 90 },
199        '71651': { cost: 174, commission: 0.27, delivery: 90 },
200        '74973': { cost: 144, commission: 0.27, delivery: 90 },
201        '73341': { cost: 130.8, commission: 0.27, delivery: 90 },
202        '75185': { cost: 157.2, commission: 0.27, delivery: 90 },
203        '74348': { cost: 132, commission: 0.27, delivery: 90 },
204        '75376': { cost: 69.6, commission: 0.27, delivery: 90 },
205        '74942': { cost: 159.6, commission: 0.27, delivery: 90 },
206        '77592': { cost: 0, commission: 0.27, delivery: 90 },
207        '74737': { cost: 183.6, commission: 0.27, delivery: 90 },
208        '76045': { cost: 235.2, commission: 0.27, delivery: 90 },
209        '74256': { cost: 186, commission: 0.27, delivery: 90 },
210        '75208': { cost: 200.4, commission: 0.27, delivery: 90 },
211        '76601': { cost: 313.2, commission: 0.27, delivery: 90 },
212        '75116': { cost: 346.8, commission: 0.27, delivery: 90 },
213        '73464': { cost: 258, commission: 0.27, delivery: 90 },
214        '74577': { cost: 134.4, commission: 0.27, delivery: 90 },
215        '73792': { cost: 120, commission: 0.27, delivery: 90 },
216        '74997': { cost: 159.6, commission: 0.27, delivery: 90 },
217        '75611': { cost: 150, commission: 0.27, delivery: 90 },
218        '74782': { cost: 145.2, commission: 0.27, delivery: 90 },
219        '75031': { cost: 87.6, commission: 0.27, delivery: 90 },
220        '74195': { cost: 171.6, commission: 0.27, delivery: 90 },
221        '75161': { cost: 96, commission: 0.27, delivery: 90 },
222        '74591': { cost: 132, commission: 0.27, delivery: 90 },
223        '20107': { cost: 283.752, commission: 0.27, delivery: 90 },
224        '74935': { cost: 331.2, commission: 0.27, delivery: 90 },
225        '75062': { cost: 99.6, commission: 0.27, delivery: 90 },
226        '74706': { cost: 130.8, commission: 0.27, delivery: 90 },
227        '75147': { cost: 470.4, commission: 0.27, delivery: 90 },
228        '73181': { cost: 234, commission: 0.27, delivery: 90 },
229        '74812': { cost: 366, commission: 0.27, delivery: 90 },
230        '74805': { cost: 421.2, commission: 0.27, delivery: 90 },
231        '74010': { cost: 112.8, commission: 0.27, delivery: 90 },
232        '73167': { cost: 98.4, commission: 0.27, delivery: 90 },
233        '74416': { cost: 91.2, commission: 0.27, delivery: 90 },
234        '75574': { cost: 1517, commission: 0.27, delivery: 90 },
235        '74546': { cost: 240, commission: 0.27, delivery: 90 },
236        '76199': { cost: 271.2, commission: 0.27, delivery: 90 },
237        '74829': { cost: 372, commission: 0.27, delivery: 90 },
238        '80173': { cost: 768, commission: 0.27, delivery: 90 },
239        '75567': { cost: 1497, commission: 0.27, delivery: 90 },
240        '73754': { cost: 392.4, commission: 0.27, delivery: 90 },
241        '76298': { cost: 637.2, commission: 0.27, delivery: 90 },
242        '74454': { cost: 88.8, commission: 0.27, delivery: 90 },
243        '76205': { cost: 273.6, commission: 0.27, delivery: 90 },
244        '76274': { cost: 662.4, commission: 0.27, delivery: 90 },
245        '76441': { cost: 124.8, commission: 0.27, delivery: 90 },
246        '76434': { cost: 145.2, commission: 0.27, delivery: 90 },
247        '80227': { cost: 768, commission: 0.27, delivery: 90 },
248        '76175': { cost: 285.6, commission: 0.27, delivery: 90 },
249        '76304': { cost: 324, commission: 0.27, delivery: 90 },
250        '71682': { cost: 99.6, commission: 0.27, delivery: 90 },
251        '74959': { cost: 183.6, commission: 0.27, delivery: 90 }
252    };
253
254    // Функция расчета прибыли
255    function calculateProfit(article, revenue, orders, drr) {
256        const costData = PRODUCT_COST_DATA[article];
257        if (!costData || !revenue || !orders) return null;
258        
259        // Расходы на рекламу = выручка * (ДРР / 100)
260        const adCost = drr ? (revenue * (drr / 100)) : 0;
261        
262        // Прибыль = Выручка - (заказы * себестоимость) - (заказы * доставка) - (выручка * комиссия) - расходы на рекламу
263        const profit = revenue - (orders * costData.cost) - (orders * costData.delivery) - (revenue * costData.commission) - adCost;
264        return Math.round(profit); // Округляем до целых
265    }
266
267    // Класс для сбора данных о товарах
268    class ProductDataCollector {
269        constructor() {
270            this.products = [];
271            this.isCollecting = false;
272        }
273
274        // Автоматическая подгрузка всех товаров
275        async loadAllProducts() {
276            console.log('📦 Начинаем загрузку всех товаров...');
277            this.isCollecting = true;
278
279            let previousCount = 0;
280            let stableCount = 0; // Счетчик стабильных попыток
281            let attempts = 0;
282            const maxAttempts = 300; // Увеличили максимум попыток до 300
283            const maxStableAttempts = 5; // Увеличили до 5 стабильных попыток
284
285            while (attempts < maxAttempts) {
286                const loadMoreBtn = document.querySelector('button.styles_loadMoreButton_2RI3D');
287                
288                if (!loadMoreBtn) {
289                    console.log('✅ Кнопка "Показать ещё" не найдена - все товары загружены');
290                    break;
291                }
292
293                // Проверяем, не отключена ли кнопка
294                if (loadMoreBtn.disabled || loadMoreBtn.classList.contains('disabled')) {
295                    console.log('✅ Кнопка "Показать ещё" отключена - все товары загружены');
296                    break;
297                }
298
299                // Проверяем, есть ли товары с нулевой выручкой (значит дошли до конца)
300                const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
301                let hasZeroRevenue = false;
302                
303                for (const row of rows) {
304                    const cells = row.querySelectorAll('td');
305                    if (cells.length >= 3) {
306                        const revenueText = cells[2].textContent.trim();
307                        // Проверяем, есть ли "0 ₽" или "0₽" в тексте выручки
308                        if (revenueText.match(/^0\s*₽/) || revenueText === '0') {
309                            hasZeroRevenue = true;
310                            console.log('✅ Найден товар с нулевой выручкой - останавливаем загрузку');
311                            break;
312                        }
313                    }
314                }
315                
316                if (hasZeroRevenue) {
317                    console.log('✅ Достигнут конец списка активных товаров');
318                    break;
319                }
320
321                // Прокручиваем к кнопке, чтобы она была видна
322                loadMoreBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
323                await delay(800);
324
325                console.log(`🔄 Клик по кнопке "Показать ещё" (попытка ${attempts + 1})`);
326                loadMoreBtn.click();
327                
328                // Увеличили задержку до 4 секунд для полной загрузки данных
329                await delay(4000);
330
331                const currentCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
332                console.log(`📊 Загружено товаров: ${currentCount} (было: ${previousCount})`);
333
334                if (currentCount === previousCount) {
335                    stableCount++;
336                    console.log(`⏸️ Количество не изменилось (${stableCount}/${maxStableAttempts})`);
337                    
338                    if (stableCount >= maxStableAttempts) {
339                        console.log('✅ Количество товаров стабильно - загрузка завершена');
340                        break;
341                    }
342                } else {
343                    stableCount = 0; // Сбрасываем счетчик, если количество изменилось
344                }
345
346                previousCount = currentCount;
347                attempts++;
348            }
349
350            const finalCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
351            console.log(`✅ Загрузка завершена. Всего товаров: ${finalCount}`);
352            this.isCollecting = false;
353        }
354
355        // Сбор данных из таблицы
356        collectProductData() {
357            console.log('📊 Собираем данные о товарах...');
358            this.products = [];
359
360            const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
361            console.log(`Найдено строк: ${rows.length}`);
362
363            rows.forEach((row, index) => {
364                try {
365                    const cells = row.querySelectorAll('td');
366                    if (cells.length < 10) return;
367
368                    // Извлекаем данные из ячеек
369                    const productData = this.extractProductData(cells);
370                    if (productData) {
371                        this.products.push(productData);
372                    }
373                } catch (error) {
374                    console.error(`Ошибка при обработке строки ${index}:`, error);
375                }
376            });
377
378            console.log(`✅ Собрано товаров: ${this.products.length}`);
379            return this.products;
380        }
381
382        // Извлечение данных о товаре из ячеек
383        extractProductData(cells) {
384            try {
385                // Название и артикул (первая ячейка)
386                const nameCell = cells[0];
387                const nameLink = nameCell.querySelector('a.styles_productName_2qRJi');
388                const captionEl = nameCell.querySelector('.styles_productCaption_7MqtH');
389                
390                const name = nameLink ? nameLink.textContent.trim() : '';
391                const articleMatch = captionEl ? captionEl.textContent.match(/Арт\.\s*(\d+)/) : null;
392                const article = articleMatch ? articleMatch[1] : '';
393
394                if (!name || !article) return null;
395
396                // Получаем текстовое содержимое всех ячеек
397                const cellTexts = Array.from(cells).map(cell => cell.textContent.trim());
398
399                // Парсим основные показатели по правильным индексам
400                // Выручка - индекс 2
401                const revenue = parseNumber(cellTexts[2]);
402                const revenueChange = parsePercent(cellTexts[2]);
403                
404                // Заказано товаров - индекс 20
405                const orders = parseNumber(cellTexts[20]);
406                const ordersChange = parsePercent(cellTexts[20]);
407                
408                // Показы всего - индекс 5
409                const impressions = parseNumber(cellTexts[5]);
410                const impressionsChange = parsePercent(cellTexts[5]);
411                
412                // Посещения карточки товара - индекс 13
413                const cardVisits = parseNumber(cellTexts[13]);
414                const cardVisitsChange = parsePercent(cellTexts[13]);
415                
416                // Конверсия из поиска и каталога в карточку (CTR) - индекс 12
417                const conversionCatalogToCard = parseNumber(cellTexts[12]);
418                const conversionCatalogToCardChange = parsePercent(cellTexts[12]);
419                
420                // Конверсия из карточки в корзину (CRL) - индекс 15
421                const conversionCardToCart = parseNumber(cellTexts[15]);
422                const conversionCardToCartChange = parsePercent(cellTexts[15]);
423                
424                // Добавления в корзину всего - индекс 18
425                const cartAdditions = parseNumber(cellTexts[18]);
426                const cartAdditionsChange = parsePercent(cellTexts[18]);
427                
428                // CR - высчитываем: Заказано товаров / Посещения карточки товаров
429                const cr = (orders && cardVisits && cardVisits > 0) ? parseFloat(((orders / cardVisits) * 100).toFixed(1)) : null;
430                const crChange = null; // Изменение CR нужно высчитывать отдельно
431                
432                // Общая ДРР - индекс 32 (парсим как процент, убираем знак %)
433                const drrText = cellTexts[32] || '';
434                const drrMatch = drrText.match(/(\d+(?:\.\d+)?)\s*%/);
435                const drr = drrMatch ? parseFloat(drrMatch[1]) : null;
436                const drrChange = parsePercent(cellTexts[32]);
437                
438                // Остаток на конец периода - индекс 35
439                const stockText = cellTexts[35] || '';
440                const stockMatch = stockText.match(/(\d+)/);
441                const stock = stockMatch ? parseInt(stockMatch[1]) : null;
442                
443                // Средняя цена - индекс 28 (используем parsePrice для корректного парсинга)
444                const avgPrice = parsePrice(cellTexts[28]);
445                const avgPriceChange = parsePercent(cellTexts[28]);
446                
447                // Среднее время доставки - индекс 37
448                const deliveryTime = cellTexts[37] || null;
449                
450                // Рассчитываем дни остатков: остаток / заказы (округляем до целых)
451                const daysOfStock = (stock && orders && orders > 0) ? Math.round(stock / orders) : null;
452                
453                // Рассчитываем прибыль
454                const profit = calculateProfit(article, revenue, orders, drr);
455                
456                // Рассчитываем прибыль в процентах от выручки
457                const profitPercent = (profit !== null && revenue && revenue > 0) ? 
458                    parseFloat(((profit / revenue) * 100).toFixed(1)) : null;
459
460                const product = {
461                    name,
462                    article,
463                    revenue,
464                    revenueChange,
465                    orders,
466                    ordersChange,
467                    impressions,
468                    impressionsChange,
469                    cardVisits,
470                    cardVisitsChange,
471                    conversionCatalogToCard,
472                    conversionCatalogToCardChange,
473                    conversionCardToCart,
474                    conversionCardToCartChange,
475                    cartAdditions,
476                    cartAdditionsChange,
477                    cr,
478                    crChange,
479                    avgPrice,
480                    avgPriceChange,
481                    drr,
482                    drrChange,
483                    stock,
484                    deliveryTime,
485                    daysOfStock,
486                    profit,
487                    profitPercent,
488                    rawData: cellTexts
489                };
490
491                return product;
492            } catch (error) {
493                console.error('Ошибка извлечения данных товара:', error);
494                return null;
495            }
496        }
497    }
498
499    // Класс для AI анализа
500    class AIAnalyzer {
501        // Батч-анализ товаров с умной фильтрацией
502        async analyzeProducts(products, onProgress) {
503            console.log('🤖 Начинаем AI анализ товаров...');
504            
505            // Сначала вычисляем средние показатели
506            const avgMetrics = this.calculateAverageMetrics(products);
507            console.log('📊 Средние показатели:', avgMetrics);
508            
509            // Разделяем товары на приоритетные и обычные
510            const priorityProducts = [];
511            const normalProducts = [];
512            
513            products.forEach(product => {
514                const needsAIAnalysis = this.needsDetailedAnalysis(product, avgMetrics);
515                if (needsAIAnalysis) {
516                    priorityProducts.push(product);
517                } else {
518                    normalProducts.push(product);
519                }
520            });
521            
522            console.log(`📊 Приоритетных товаров для AI анализа: ${priorityProducts.length}`);
523            console.log(`📊 Обычных товаров (базовый анализ): ${normalProducts.length}`);
524            
525            const analyzedProducts = [];
526            const batchSize = 10; // Увеличили до 10 товаров одновременно
527            
528            // Сначала быстро обрабатываем обычные товары (без AI)
529            normalProducts.forEach(product => {
530                analyzedProducts.push({
531                    ...product,
532                    analysis: this.basicAnalysis(product, avgMetrics)
533                });
534            });
535            
536            // Обновляем прогресс после базового анализа
537            if (onProgress) {
538                const percentage = Math.round((normalProducts.length / products.length) * 100);
539                const remaining = Math.ceil((priorityProducts.length / batchSize) * 2);
540                onProgress(normalProducts.length, products.length, percentage, remaining);
541            }
542            
543            // Анализируем приоритетные товары с AI
544            for (let i = 0; i < priorityProducts.length; i += batchSize) {
545                const batch = priorityProducts.slice(i, i + batchSize);
546                const batchPromises = batch.map(product => this.analyzeProduct(product, avgMetrics, true));
547                
548                const batchResults = await Promise.all(batchPromises);
549                
550                batchResults.forEach((analysis, idx) => {
551                    analyzedProducts.push({
552                        ...batch[idx],
553                        analysis
554                    });
555                });
556                
557                const progress = Math.min(i + batchSize, priorityProducts.length);
558                const totalProgress = normalProducts.length + progress;
559                const percentage = Math.round((totalProgress / products.length) * 100);
560                const remaining = Math.ceil(((priorityProducts.length - progress) / batchSize) * 2);
561                
562                if (onProgress) {
563                    onProgress(totalProgress, products.length, percentage, remaining);
564                }
565                
566                console.log(`✅ Проанализировано ${progress} из ${priorityProducts.length} приоритетных товаров`);
567            }
568            
569            if (onProgress) {
570                onProgress(products.length, products.length, 100, 0);
571            }
572
573            return analyzedProducts;
574        }
575
576        // Определяем, нужен ли детальный AI анализ
577        needsDetailedAnalysis(product, avgMetrics) {
578            const threshold = 5; // Порог отклонения 5%
579            
580            // Если есть значительное падение выручки
581            if (product.revenueChange !== null && product.revenueChange < avgMetrics.revenueChange - threshold) {
582                return true;
583            }
584            
585            // Если есть значительное падение заказов
586            if (product.ordersChange !== null && product.ordersChange < avgMetrics.ordersChange - threshold) {
587                return true;
588            }
589            
590            // Если высокий ДРР
591            if (product.drr !== null && product.drr > 20) {
592                return true;
593            }
594            
595            // Если низкие остатки
596            const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
597            if (daysOfStock !== null && daysOfStock < 49) {
598                return true;
599            }
600            
601            // Если значительный рост (для масштабирования)
602            if (product.revenueChange !== null && product.revenueChange > avgMetrics.revenueChange + 15) {
603                return true;
604            }
605            
606            return false;
607        }
608
609        // Базовый анализ без AI (для товаров без проблем)
610        basicAnalysis(product, avgMetrics) {
611            const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
612            const isLowStock = daysOfStock !== null && daysOfStock <= 14;
613            const isHighDRR = product.drr !== null && product.drr > 20;
614            const isLowDRR = product.drr !== null && product.drr <= 17;
615            const isGrowth = this.detectGrowth(product, avgMetrics);
616            const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
617            const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) || 
618                           (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
619            const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 && 
620                               (product.profit / product.revenue) < 0.25;
621            
622            // Проверяем время доставки (парсим число из строки типа "35 ч")
623            const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
624            const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
625
626            // Генерируем рекомендации на основе проблем
627            const recommendations = [];
628            
629            if (isLowImpressions) {
630                // Проверяем ДРР - если снизился, рекомендуем увеличить бюджет
631                if (product.drrChange !== null && product.drrChange < 0) {
632                    recommendations.push('Показы упали. ДРР снизился - рекомендуем увеличить бюджет рекламы');
633                }
634                // Проверяем время доставки - если больше 3 дней, это снижает видимость
635                if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
636                    recommendations.push('Показы упали. Время доставки высокое - снижает видимость');
637                }
638                // Проверяем остатки - если маленькие, рекомендуем пополнить
639                if (isLowStock) {
640                    recommendations.push('Показы упали. Низкие остатки - рекомендуем пополнить склад');
641                }
642                // Если нет конкретных причин
643                if (recommendations.length === 0) {
644                    recommendations.push('Показы упали. Проверьте настройки рекламы и позиции товара');
645                }
646            }
647            
648            if (isLowCR) {
649                // Проверяем среднюю цену - если выросла, рекомендуем снизить
650                if (product.avgPriceChange !== null && product.avgPriceChange > 0) {
651                    recommendations.push('CR упал. Цена выросла - рекомендуем снизить цену для повышения конверсии');
652                }
653                // Проверяем время доставки
654                if (product.deliveryTime && parseInt(product.deliveryTime) > 3) {
655                    recommendations.push('CR упал. Время доставки высокое - снижает конверсию');
656                }
657                // Проверяем остатки
658                if (isLowStock) {
659                    recommendations.push('CR упал. Низкие остатки - снижает конверсию');
660                }
661                // Если нет конкретных причин
662                if (recommendations.length === 0) {
663                    recommendations.push('CR упал. Проверьте карточку товара, фото и описание');
664                }
665            }
666            
667            if (isLowProfit) {
668                recommendations.push('Низкая прибыль. Проверьте себестоимость, цену и рекламные расходы');
669            }
670            
671            if (isLowStock && !isLowImpressions && !isLowCR) {
672                recommendations.push('Низкие остатки - рекомендуем пополнить склад');
673            }
674            
675            if (isHighDRR && !isLowImpressions && !isLowCR) {
676                recommendations.push('Высокий ДРР - рекомендуем оптимизировать рекламные кампании');
677            }
678            
679            // Если нет проблем - выводим "Всё хорошо"
680            if (recommendations.length === 0) {
681                recommendations.push('Всё хорошо, рекомендаций нет');
682            }
683            
684            return {
685                priority: 'low',
686                problems: [],
687                recommendations,
688                daysOfStock,
689                isLowStock,
690                isHighDRR,
691                isLowDRR,
692                isGrowth,
693                isLowImpressions,
694                isLowCR,
695                isLowProfit,
696                isBadDeliveryTime
697            };
698        }
699
700        // Вычисление средних показателей
701        calculateAverageMetrics(products) {
702            const validProducts = products.filter(p => p.revenueChange !== null);
703            if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
704            
705            const sum = validProducts.reduce((acc, p) => ({
706                revenueChange: acc.revenueChange + (p.revenueChange || 0),
707                ordersChange: acc.ordersChange + (p.ordersChange || 0),
708                impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
709            }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
710            
711            return {
712                revenueChange: sum.revenueChange / validProducts.length,
713                ordersChange: sum.ordersChange / validProducts.length,
714                impressionsChange: sum.impressionsChange / validProducts.length
715            };
716        }
717
718        async analyzeProduct(product, avgMetrics, useAI = true) {
719            try {
720                const daysOfStock = product.orders && product.stock ? Math.floor(product.stock / (product.orders / 7)) : null;
721                const isLowStock = daysOfStock !== null && daysOfStock < 49;
722                const isHighDRR = product.drr !== null && product.drr > 20;
723                const isLowDRR = product.drr !== null && product.drr <= 17;
724                const isGrowth = this.detectGrowth(product, avgMetrics);
725                const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
726                const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) || 
727                               (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
728                const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 && 
729                                   (product.profit / product.revenue) < 0.25;
730                
731                // Проверяем время доставки
732                const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
733                const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
734                
735                if (!useAI) {
736                    return this.basicAnalysis(product, avgMetrics);
737                }
738                
739                // Формируем промпт для AI
740                const prompt = `Проанализируй товар и определи проблемы:
741
742Товар: ${product.name}
743Показатели:
744- Выручка: ${product.revenue || 'н/д'} ₽ (${product.revenueChange || 0}%)
745- Заказы: ${product.orders || 'н/д'} (${product.ordersChange || 0}%)
746- Показы: ${product.impressions || 'н/д'} (${product.impressionsChange || 0}%)
747- Посещения карточки: ${product.cardVisits || 'н/д'} (${product.cardVisitsChange || 0}%)
748- CTR: ${product.conversionCatalogToCard || 'н/д'}%
749- CRL: ${product.conversionCardToCart || 'н/д'}%
750- CR: ${product.cr || 'н/д'}%
751- ДРР: ${product.drr || 'н/д'}%
752- Остаток: ${product.stock || 'н/д'} шт (хватит на ${daysOfStock || 'н/д'} дней)
753
754Определи приоритет (critical/high/medium/low), проблемы и рекомендации.`;
755
756                const response = await RM.aiCall(prompt, {
757                    type: 'json_schema',
758                    json_schema: {
759                        name: 'product_analysis',
760                        schema: {
761                            type: 'object',
762                            properties: {
763                                priority: {
764                                    type: 'string',
765                                    enum: ['critical', 'high', 'medium', 'low']
766                                },
767                                problems: {
768                                    type: 'array',
769                                    items: {
770                                        type: 'object',
771                                        properties: {
772                                            type: { type: 'string' },
773                                            description: { type: 'string' }
774                                        },
775                                        required: ['type', 'description']
776                                    }
777                                },
778                                recommendations: {
779                                    type: 'array',
780                                    items: { type: 'string' }
781                                }
782                            },
783                            required: ['priority', 'problems', 'recommendations']
784                        }
785                    }
786                });
787
788                return {
789                    ...response,
790                    daysOfStock,
791                    isLowStock,
792                    isHighDRR,
793                    isLowDRR,
794                    isGrowth,
795                    isLowImpressions,
796                    isLowCR,
797                    isLowProfit,
798                    isBadDeliveryTime
799                };
800            } catch (error) {
801                console.error('Ошибка AI анализа:', error);
802                return this.basicAnalysis(product, avgMetrics);
803            }
804        }
805
806        // Определение роста на основе средних показателей
807        detectGrowth(product, avgMetrics) {
808            const threshold = 15; // Порог отклонения от среднего в %
809            
810            // Если выручка растет значительно выше среднего
811            if (product.revenueChange !== null && 
812                product.revenueChange > avgMetrics.revenueChange + threshold) {
813                return true;
814            }
815            
816            // Если заказы растут значительно выше среднего
817            if (product.ordersChange !== null && 
818                product.ordersChange > avgMetrics.ordersChange + threshold) {
819                return true;
820            }
821            
822            return false;
823        }
824    }
825
826    // Класс для UI
827    class AnalyticsUI {
828        constructor() {
829            this.container = null;
830            this.filteredProducts = [];
831            this.allProducts = [];
832            this.currentFilter = 'all';
833            this.isCollapsed = false;
834            this.isDragging = false;
835            this.isResizing = false;
836            this.dragStartX = 0;
837            this.dragStartY = 0;
838            this.containerStartX = 0;
839            this.containerStartY = 0;
840            this.resizeStartWidth = 0;
841            this.resizeStartHeight = 0;
842        }
843
844        createUI() {
845            console.log('🎨 Создаем UI...');
846
847            // Создаем контейнер для нашего UI
848            this.container = document.createElement('div');
849            this.container.id = 'ozon-ai-analytics';
850            this.container.style.cssText = `
851                position: fixed;
852                top: 80px;
853                right: 20px;
854                width: 500px;
855                max-height: 85vh;
856                background: white;
857                border-radius: 12px;
858                box-shadow: 0 4px 20px rgba(0,0,0,0.15);
859                z-index: 10000;
860                overflow: hidden;
861                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
862                resize: both;
863                min-width: 400px;
864                min-height: 200px;
865            `;
866
867            // Заголовок (с возможностью перетаскивания)
868            const header = document.createElement('div');
869            header.id = 'ozon-ai-header';
870            header.style.cssText = `
871                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
872                color: white;
873                padding: 18px 24px;
874                font-weight: 600;
875                font-size: 18px;
876                display: flex;
877                justify-content: space-between;
878                align-items: center;
879                cursor: move;
880                user-select: none;
881            `;
882            header.innerHTML = `
883                <span>🤖 AI Аналитик Продаж</span>
884                <div style="display: flex; gap: 8px; align-items: center;">
885                    <button id="ozon-ai-collapse" style="background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Свернуть/Развернуть"></button>
886                    <button id="ozon-ai-close" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; padding: 0; width: 28px; height: 28px;" title="Закрыть">×</button>
887                </div>
888            `;
889
890            // Кнопка запуска анализа
891            const startButton = document.createElement('button');
892            startButton.id = 'ozon-ai-start';
893            startButton.textContent = '🚀 Запустить анализ';
894            startButton.style.cssText = `
895                width: calc(100% - 40px);
896                margin: 20px;
897                padding: 16px;
898                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
899                color: white;
900                border: none;
901                border-radius: 8px;
902                font-size: 16px;
903                font-weight: 600;
904                cursor: pointer;
905                transition: transform 0.2s;
906            `;
907            startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
908            startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
909
910            // Контейнер для контента
911            const content = document.createElement('div');
912            content.id = 'ozon-ai-content';
913            content.style.cssText = `
914                padding: 20px;
915                max-height: calc(85vh - 140px);
916                overflow-y: auto;
917            `;
918
919            // Индикатор изменения размера
920            const resizeHandle = document.createElement('div');
921            resizeHandle.id = 'ozon-ai-resize';
922            resizeHandle.style.cssText = `
923                position: absolute;
924                bottom: 0;
925                right: 0;
926                width: 20px;
927                height: 20px;
928                cursor: nwse-resize;
929                background: linear-gradient(135deg, transparent 0%, transparent 50%, #667eea 50%, #667eea 100%);
930                border-bottom-right-radius: 12px;
931            `;
932
933            this.container.appendChild(header);
934            this.container.appendChild(startButton);
935            this.container.appendChild(content);
936            this.container.appendChild(resizeHandle);
937
938            document.body.appendChild(this.container);
939
940            // События для перетаскивания
941            header.addEventListener('mousedown', (e) => this.startDragging(e));
942            document.addEventListener('mousemove', (e) => this.drag(e));
943            document.addEventListener('mouseup', () => this.stopDragging());
944
945            // События для изменения размера
946            resizeHandle.addEventListener('mousedown', (e) => this.startResizing(e));
947
948            // События кнопок
949            document.getElementById('ozon-ai-close').addEventListener('click', () => {
950                this.container.style.display = 'none';
951            });
952
953            document.getElementById('ozon-ai-collapse').addEventListener('click', () => {
954                this.toggleCollapse();
955            });
956
957            document.getElementById('ozon-ai-start').addEventListener('click', () => {
958                this.startAnalysis();
959            });
960
961            console.log('✅ UI создан');
962        }
963
964        startDragging(e) {
965            if (e.target.closest('button')) return; // Не перетаскиваем при клике на кнопки
966            this.isDragging = true;
967            this.dragStartX = e.clientX;
968            this.dragStartY = e.clientY;
969            const rect = this.container.getBoundingClientRect();
970            this.containerStartX = rect.left;
971            this.containerStartY = rect.top;
972            this.container.style.transition = 'none';
973        }
974
975        drag(e) {
976            if (this.isDragging) {
977                const deltaX = e.clientX - this.dragStartX;
978                const deltaY = e.clientY - this.dragStartY;
979                this.container.style.left = `${this.containerStartX + deltaX}px`;
980                this.container.style.top = `${this.containerStartY + deltaY}px`;
981                this.container.style.right = 'auto';
982            } else if (this.isResizing) {
983                const deltaX = e.clientX - this.dragStartX;
984                const deltaY = e.clientY - this.dragStartY;
985                const newWidth = Math.max(400, this.resizeStartWidth + deltaX);
986                const newHeight = Math.max(200, this.resizeStartHeight + deltaY);
987                this.container.style.width = `${newWidth}px`;
988                this.container.style.maxHeight = `${newHeight}px`;
989            }
990        }
991
992        stopDragging() {
993            this.isDragging = false;
994            this.isResizing = false;
995            this.container.style.transition = '';
996        }
997
998        startResizing(e) {
999            e.stopPropagation();
1000            this.isResizing = true;
1001            this.dragStartX = e.clientX;
1002            this.dragStartY = e.clientY;
1003            this.resizeStartWidth = this.container.offsetWidth;
1004            this.resizeStartHeight = this.container.offsetHeight;
1005        }
1006
1007        toggleCollapse() {
1008            this.isCollapsed = !this.isCollapsed;
1009            const content = document.getElementById('ozon-ai-content');
1010            const startButton = document.getElementById('ozon-ai-start');
1011            const resizeHandle = document.getElementById('ozon-ai-resize');
1012            const collapseButton = document.getElementById('ozon-ai-collapse');
1013            
1014            if (this.isCollapsed) {
1015                content.style.display = 'none';
1016                startButton.style.display = 'none';
1017                resizeHandle.style.display = 'none';
1018                collapseButton.textContent = '+';
1019                this.container.style.maxHeight = 'auto';
1020            } else {
1021                content.style.display = 'block';
1022                startButton.style.display = 'block';
1023                resizeHandle.style.display = 'block';
1024                collapseButton.textContent = '−';
1025                this.container.style.maxHeight = '85vh';
1026            }
1027        }
1028
1029        async startAnalysis() {
1030            const content = document.getElementById('ozon-ai-content');
1031            const startButton = document.getElementById('ozon-ai-start');
1032            
1033            startButton.disabled = true;
1034            startButton.textContent = '⏳ Загрузка товаров...';
1035
1036            try {
1037                // Шаг 1: Загрузка всех товаров
1038                const collector = new ProductDataCollector();
1039                await collector.loadAllProducts();
1040
1041                startButton.textContent = '📊 Сбор данных...';
1042                
1043                // Шаг 2: Сбор данных
1044                const products = collector.collectProductData();
1045
1046                if (products.length === 0) {
1047                    content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
1048                    startButton.disabled = false;
1049                    startButton.textContent = '🚀 Запустить анализ';
1050                    return;
1051                }
1052
1053                // Шаг 3: AI анализ с прогрессом
1054                const analyzer = new AIAnalyzer();
1055                
1056                const onProgress = (current, total, percentage, remaining) => {
1057                    const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
1058                    startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
1059                };
1060                
1061                const analyzedProducts = await analyzer.analyzeProducts(products, onProgress);
1062
1063                this.allProducts = analyzedProducts;
1064                this.filteredProducts = analyzedProducts;
1065
1066                // Шаг 4: Отображение результатов
1067                this.displayResults(analyzedProducts);
1068
1069                startButton.textContent = '✅ Анализ завершен';
1070                startButton.disabled = false;
1071
1072            } catch (error) {
1073                console.error('Ошибка анализа:', error);
1074                content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
1075                startButton.disabled = false;
1076                startButton.textContent = '🚀 Запустить анализ';
1077            }
1078        }
1079
1080        displayResults(products) {
1081            const content = document.getElementById('ozon-ai-content');
1082            
1083            // Фильтры
1084            const filters = this.createFilters(products);
1085            
1086            // Список товаров
1087            const productsList = this.createProductsList(products);
1088            content.innerHTML = '';
1089            content.appendChild(filters);
1090            content.appendChild(productsList);
1091        }
1092
1093        createFilters(products) {
1094            const filtersContainer = document.createElement('div');
1095            filtersContainer.style.cssText = `
1096                margin-bottom: 20px;
1097            `;
1098
1099            // Поле поиска
1100            const searchContainer = document.createElement('div');
1101            searchContainer.style.cssText = `
1102                margin-bottom: 12px;
1103            `;
1104            
1105            const searchInput = document.createElement('input');
1106            searchInput.type = 'text';
1107            searchInput.placeholder = '🔍 Поиск по названию или артикулу...';
1108            searchInput.id = 'ozon-ai-search';
1109            searchInput.style.cssText = `
1110                width: 100%;
1111                padding: 10px 12px;
1112                border: 2px solid #ecf0f1;
1113                border-radius: 6px;
1114                font-size: 14px;
1115                font-family: inherit;
1116                outline: none;
1117                transition: border-color 0.2s;
1118            `;
1119            
1120            searchInput.addEventListener('focus', () => {
1121                searchInput.style.borderColor = '#667eea';
1122            });
1123            
1124            searchInput.addEventListener('blur', () => {
1125                searchInput.style.borderColor = '#ecf0f1';
1126            });
1127            
1128            searchInput.addEventListener('input', (e) => {
1129                this.applySearch(e.target.value);
1130            });
1131            
1132            searchContainer.appendChild(searchInput);
1133            filtersContainer.appendChild(searchContainer);
1134
1135            // Кнопки фильтров
1136            const buttonsContainer = document.createElement('div');
1137            buttonsContainer.style.cssText = `
1138                display: flex;
1139                flex-wrap: wrap;
1140                gap: 8px;
1141            `;
1142
1143            // Подсчет товаров по категориям
1144            const critical = products.filter(p => p.analysis.priority === 'critical').length;
1145            const high = products.filter(p => p.analysis.priority === 'high').length;
1146            const lowStock = products.filter(p => p.analysis.isLowStock).length;
1147            const highDRR = products.filter(p => p.analysis.isHighDRR).length;
1148            const lowDRR = products.filter(p => p.analysis.isLowDRR).length;
1149            const growth = products.filter(p => p.analysis.isGrowth).length;
1150            const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
1151            const lowCR = products.filter(p => p.analysis.isLowCR).length;
1152            const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
1153            const badDeliveryTime = products.filter(p => p.analysis.isBadDeliveryTime).length;
1154
1155            const filterButtons = [
1156                { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
1157                { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
1158                { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
1159                { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
1160                { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
1161                { id: 'lowDRR', label: `📊 Повысить ДРР (${lowDRR})`, color: '#16a085' },
1162                { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
1163                { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
1164                { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
1165                { id: 'badDeliveryTime', label: `⏱️ Плохое время (${badDeliveryTime})`, color: '#8e44ad' },
1166                { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
1167            ];
1168
1169            filterButtons.forEach(filter => {
1170                const btn = document.createElement('button');
1171                btn.textContent = filter.label;
1172                btn.style.cssText = `
1173                    padding: 8px 12px;
1174                    background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
1175                    color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
1176                    border: none;
1177                    border-radius: 6px;
1178                    font-size: 13px;
1179                    font-weight: 500;
1180                    cursor: pointer;
1181                    transition: all 0.2s;
1182                `;
1183                
1184                btn.addEventListener('click', () => {
1185                    this.currentFilter = filter.id;
1186                    this.applyFilter(filter.id);
1187                });
1188
1189                buttonsContainer.appendChild(btn);
1190            });
1191
1192            filtersContainer.appendChild(buttonsContainer);
1193            return filtersContainer;
1194        }
1195
1196        applySearch(searchTerm) {
1197            const term = searchTerm.toLowerCase().trim();
1198            
1199            console.log(`🔍 Поиск по запросу: "${term}"`);
1200            
1201            if (!term) {
1202                // Если поиск пустой, применяем текущий фильтр
1203                this.applyFilter(this.currentFilter);
1204                return;
1205            }
1206            
1207            // Фильтруем по поисковому запросу
1208            const filtered = this.allProducts.filter(p => {
1209                const nameMatch = p.name.toLowerCase().includes(term);
1210                const articleMatch = p.article.includes(term);
1211                return nameMatch || articleMatch;
1212            });
1213            
1214            console.log(`✅ Найдено товаров: ${filtered.length}`);
1215            
1216            // Сортируем по выручке
1217            filtered.sort((a, b) => {
1218                const revenueA = a.revenue || 0;
1219                const revenueB = b.revenue || 0;
1220                return revenueB - revenueA;
1221            });
1222            
1223            this.filteredProducts = filtered;
1224            
1225            // Обновляем только список товаров, не трогая фильтры
1226            const content = document.getElementById('ozon-ai-content');
1227            const productsList = this.createProductsList(filtered);
1228            
1229            // Находим и удаляем только список товаров (второй div в content)
1230            const children = content.children;
1231            if (children.length > 1) {
1232                children[1].remove();
1233            }
1234            
1235            content.appendChild(productsList);
1236        }
1237
1238        applyFilter(filterId) {
1239            let filtered = this.allProducts;
1240            
1241            console.log(`🔍 Применяем фильтр: ${filterId}`);
1242            
1243            // Очищаем поле поиска при смене фильтра
1244            const searchInput = document.getElementById('ozon-ai-search');
1245            if (searchInput) {
1246                searchInput.value = '';
1247            }
1248
1249            switch(filterId) {
1250            case 'critical':
1251                filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
1252                break;
1253            case 'high':
1254                filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
1255                break;
1256            case 'lowStock':
1257                filtered = this.allProducts.filter(p => p.analysis.isLowStock);
1258                break;
1259            case 'highDRR':
1260                filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
1261                break;
1262            case 'lowDRR':
1263                filtered = this.allProducts.filter(p => p.analysis.isLowDRR);
1264                break;
1265            case 'lowImpressions':
1266                filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
1267                console.log('📊 Товары с упавшими показами:', filtered.map(p => `${p.article} (${p.impressionsChange}%)`));
1268                break;
1269            case 'lowCR':
1270                filtered = this.allProducts.filter(p => p.analysis.isLowCR);
1271                break;
1272            case 'lowProfit':
1273                filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
1274                break;
1275            case 'badDeliveryTime':
1276                filtered = this.allProducts.filter(p => p.analysis.isBadDeliveryTime);
1277                break;
1278            case 'growth':
1279                filtered = this.allProducts.filter(p => p.analysis.isGrowth);
1280                break;
1281            }
1282
1283            console.log(`✅ Найдено товаров: ${filtered.length}`);
1284
1285            // Сортируем по выручке (от большей к меньшей)
1286            filtered.sort((a, b) => {
1287                const revenueA = a.revenue || 0;
1288                const revenueB = b.revenue || 0;
1289                return revenueB - revenueA;
1290            });
1291
1292            this.filteredProducts = filtered;
1293            this.displayResults(filtered);
1294        }
1295
1296        createProductsList(products) {
1297            const list = document.createElement('div');
1298            list.style.cssText = `
1299                display: flex;
1300                flex-direction: column;
1301                gap: 12px;
1302            `;
1303
1304            products.forEach(product => {
1305                const card = this.createProductCard(product);
1306                list.appendChild(card);
1307            });
1308
1309            return list;
1310        }
1311
1312        formatMetric(value, change, isPercent = false) {
1313            // Округляем проценты до десятых
1314            let displayValue = value;
1315            if (isPercent && value !== null && value !== undefined) {
1316                displayValue = parseFloat(value.toFixed(1));
1317            }
1318            
1319            const valueStr = displayValue !== null && displayValue !== undefined ? 
1320                (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
1321            
1322            if (change === null || change === undefined) return valueStr;
1323            
1324            // Округляем изменение до десятых
1325            const roundedChange = parseFloat(change.toFixed(1));
1326            const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
1327            const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
1328            
1329            return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
1330        }
1331
1332        createProductCard(product) {
1333            const card = document.createElement('div');
1334            
1335            const priorityColors = {
1336                critical: '#e74c3c',
1337                high: '#f39c12',
1338                medium: '#3498db',
1339                low: '#95a5a6'
1340            };
1341
1342            const priorityLabels = {
1343                critical: '🔴 Критичный',
1344                high: '🟠 Высокий',
1345                medium: '🟡 Средний',
1346                low: '🟢 Низкий'
1347            };
1348
1349            // Определяем цвет прибыли
1350            const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1351
1352            card.style.cssText = `
1353                background: white;
1354                border: 2px solid ${priorityColors[product.analysis.priority]};
1355                border-radius: 8px;
1356                padding: 14px;
1357                cursor: pointer;
1358                transition: all 0.2s;
1359            `;
1360
1361            card.innerHTML = `
1362                <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
1363                    <div style="flex: 1;">
1364                        <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
1365                        <div class="article-copy" style="font-size: 12px; color: #7f8c8d; cursor: pointer; user-select: none;" title="Нажмите, чтобы скопировать артикул">Арт. ${product.article}</div>
1366                    </div>
1367                    <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
1368                        ${priorityLabels[product.analysis.priority]}
1369                    </div>
1370                </div>
1371
1372                ${product.analysis.problems.length > 0 ? `
1373                <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 500; color: #856404;">
1374                    ${product.analysis.problems.slice(0, 2).map(p => `
1375                        <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
1376                    `).join('')}
1377                </div>
1378                ` : ''}
1379
1380                <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 12px;">
1381                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1382                        <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)}</div>
1383                        <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()}` : '—'} ${product.profitPercent !== null ? `(${product.profitPercent}%)` : ''}</span></div>
1384                        <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1385                        <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1386                        <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
1387                        <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1388                        <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1389                        <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1390                        <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
1391                        <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
1392                        <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)}</div>
1393                        <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
1394                        <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
1395                        <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
1396                    </div>
1397                </div>
1398
1399                <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
1400                    <strong>Рекомендации:</strong>
1401                    ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
1402                </div>
1403            `;
1404
1405            // Обработчик копирования артикула
1406            const articleElement = card.querySelector('.article-copy');
1407            articleElement.addEventListener('click', async (e) => {
1408                e.stopPropagation();
1409                try {
1410                    await GM.setClipboard(product.article);
1411                    const originalText = articleElement.textContent;
1412                    articleElement.textContent = '✓ Скопировано!';
1413                    articleElement.style.color = '#27ae60';
1414                    setTimeout(() => {
1415                        articleElement.textContent = originalText;
1416                        articleElement.style.color = '#7f8c8d';
1417                    }, 1500);
1418                } catch (error) {
1419                    console.error('Ошибка копирования:', error);
1420                }
1421            });
1422
1423            card.addEventListener('click', () => {
1424                this.showProductDetails(product);
1425            });
1426
1427            return card;
1428        }
1429
1430        showProductDetails(product) {
1431            // Определяем цвет прибыли
1432            const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
1433
1434            // Создаем модальное окно с детальной информацией
1435            const modal = document.createElement('div');
1436            modal.style.cssText = `
1437                position: fixed;
1438                top: 0;
1439                left: 0;
1440                right: 0;
1441                bottom: 0;
1442                background: rgba(0,0,0,0.7);
1443                z-index: 10001;
1444                display: flex;
1445                align-items: center;
1446                justify-content: center;
1447                padding: 20px;
1448            `;
1449
1450            const modalContent = document.createElement('div');
1451            modalContent.style.cssText = `
1452                background: white;
1453                border-radius: 12px;
1454                padding: 24px;
1455                max-width: 700px;
1456                max-height: 80vh;
1457                overflow-y: auto;
1458                width: 100%;
1459            `;
1460
1461            modalContent.innerHTML = `
1462                <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
1463                    <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
1464                    <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
1465                </div>
1466
1467                <div style="background: #f8f9fa; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
1468                    <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 12px;">Артикул: ${product.article}</div>
1469                    
1470                    <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 13px;">
1471                        <div>
1472                            <div style="color: #7f8c8d; font-size: 11px;">Выручка</div>
1473                            <div style="font-weight: 600;">${this.formatMetric(product.revenue, product.revenueChange)}</div>
1474                        </div>
1475                        
1476                        <div>
1477                            <div style="color: #7f8c8d; font-size: 11px;">Прибыль</div>
1478                            <div style="font-weight: 600; color: ${profitColor};">${product.profit !== null ? `${product.profit.toLocaleString()}` : '—'}</div>
1479                        </div>
1480                        
1481                        <div>
1482                            <div style="color: #7f8c8d; font-size: 11px;">Прибыль %</div>
1483                            <div style="font-weight: 600; color: ${profitColor};">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</div>
1484                        </div>
1485                        
1486                        <div>
1487                            <div style="color: #7f8c8d; font-size: 11px;">Заказы</div>
1488                            <div style="font-weight: 600;">${this.formatMetric(product.orders, product.ordersChange)}</div>
1489                        </div>
1490
1491                        <div>
1492                            <div style="color: #7f8c8d; font-size: 11px;">Показы всего</div>
1493                            <div style="font-weight: 600;">${this.formatMetric(product.impressions, product.impressionsChange)}</div>
1494                        </div>
1495
1496                        <div>
1497                            <div style="color: #7f8c8d; font-size: 11px;">Посещения карточки</div>
1498                            <div style="font-weight: 600;">${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
1499                        </div>
1500
1501                        <div>
1502                            <div style="color: #7f8c8d; font-size: 11px;">Добавления в корзину</div>
1503                            <div style="font-weight: 600;">${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
1504                        </div>
1505
1506                        <div>
1507                            <div style="color: #7f8c8d; font-size: 11px;">CTR (каталог→карточка)</div>
1508                            <div style="font-weight: 600;">${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
1509                        </div>
1510
1511                        <div>
1512                            <div style="color: #7f8c8d; font-size: 11px;">CRL (карточка→корзина)</div>
1513                            <div style="font-weight: 600;">${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
1514                        </div>
1515
1516                        <div>
1517                            <div style="color: #7f8c8d; font-size: 11px;">CR (заказы/карточка)</div>
1518                            <div style="font-weight: 600;">${this.formatMetric(product.cr, product.crChange, true)}</div>
1519                        </div>
1520
1521                        <div>
1522                            <div style="color: #7f8c8d; font-size: 11px;">ДРР</div>
1523                            <div style="font-weight: 600;">${this.formatMetric(product.drr, product.drrChange, true)}</div>
1524                        </div>
1525
1526                        <div>
1527                            <div style="color: #7f8c8d; font-size: 11px;">Время доставки</div>
1528                            <div style="font-weight: 600;">${product.deliveryTime || '—'}</div>
1529                        </div>
1530
1531                        <div>
1532                            <div style="color: #7f8c8d; font-size: 11px;">Остаток на конец периода</div>
1533                            <div style="font-weight: 600;">${product.stock || '—'} шт</div>
1534                        </div>
1535
1536                        <div>
1537                            <div style="color: #7f8c8d; font-size: 11px;">Хватит на дней</div>
1538                            <div style="font-weight: 600;">${product.daysOfStock || '—'}</div>
1539                        </div>
1540                    </div>
1541                </div>
1542
1543                <div style="margin-bottom: 16px;">
1544                    <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
1545                    ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
1546                        <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1547                            <strong>${p.type}:</strong> ${p.description}
1548                        </div>
1549                    `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
1550                </div>
1551
1552                <div>
1553                    <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
1554                    ${product.analysis.recommendations.map(r => `
1555                        <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
1556                            ${r}
1557                        </div>
1558                    `).join('')}
1559                </div>
1560
1561                <button id="filter-by-article" style="
1562                    width: 100%;
1563                    margin-top: 16px;
1564                    padding: 12px;
1565                    background: #667eea;
1566                    color: white;
1567                    border: none;
1568                    border-radius: 8px;
1569                    font-size: 14px;
1570                    font-weight: 600;
1571                    cursor: pointer;
1572                ">
1573                    🔍 Показать только этот товар в таблице
1574                </button>
1575            `;
1576
1577            modal.appendChild(modalContent);
1578            document.body.appendChild(modal);
1579
1580            // Закрытие модального окна
1581            modal.addEventListener('click', (e) => {
1582                if (e.target === modal) {
1583                    modal.remove();
1584                }
1585            });
1586
1587            modalContent.querySelector('#close-modal').addEventListener('click', () => {
1588                modal.remove();
1589            });
1590
1591            // Фильтрация по артикулу
1592            modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
1593                this.filterByArticle(product.article);
1594                modal.remove();
1595            });
1596        }
1597
1598        filterByArticle(article) {
1599            console.log(`🔍 Фильтруем по артикулу: ${article}`);
1600            
1601            // Находим поле фильтра по артикулу на странице
1602            const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
1603            
1604            if (articleInput) {
1605                articleInput.value = article;
1606                articleInput.dispatchEvent(new Event('input', { bubbles: true }));
1607                articleInput.dispatchEvent(new Event('change', { bubbles: true }));
1608                
1609                // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
1610                const buttons = document.querySelectorAll('button[type="submit"]');
1611                let applyButton = null;
1612                for (const btn of buttons) {
1613                    if (btn.textContent.includes('Применить')) {
1614                        applyButton = btn;
1615                        break;
1616                    }
1617                }
1618                
1619                if (applyButton) {
1620                    setTimeout(() => applyButton.click(), 300);
1621                }
1622            } else {
1623                console.warn('Не найдено поле для ввода артикула');
1624            }
1625        }
1626    }
1627
1628    // Инициализация
1629    async function init() {
1630        console.log('🎯 Инициализация AI Аналитика Продаж...');
1631
1632        // Проверяем, что мы на странице аналитики
1633        if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
1634            console.log('⚠️ Не на странице аналитики, ожидаем...');
1635            return;
1636        }
1637
1638        // Ждем загрузки таблицы
1639        const waitForTable = setInterval(() => {
1640            const table = document.querySelector('table.ct590-a');
1641            if (table) {
1642                clearInterval(waitForTable);
1643                console.log('✅ Таблица найдена, создаем UI');
1644                
1645                const ui = new AnalyticsUI();
1646                ui.createUI();
1647            }
1648        }, 1000);
1649    }
1650
1651    // Запуск при загрузке страницы
1652    if (document.readyState === 'loading') {
1653        document.addEventListener('DOMContentLoaded', init);
1654    } else {
1655        init();
1656    }
1657
1658    // Отслеживание изменений URL (для SPA)
1659    let lastUrl = location.href;
1660    new MutationObserver(() => {
1661        const url = location.href;
1662        if (url !== lastUrl) {
1663            lastUrl = url;
1664            init();
1665        }
1666    }).observe(document, { subtree: true, childList: true });
1667
1668})();
Ozon AI Analyzer 2.0 | Robomonkey