Ozon Product Parser

Парсер товаров с Ozon для анализа поисковой выдачи

Size

139.5 KB

Version

1.8.9

Created

Dec 11, 2025

Updated

23 days ago

1// ==UserScript==
2// @name		Ozon Product Parser
3// @description		Парсер товаров с Ozon для анализа поисковой выдачи
4// @version		1.8.9
5// @match		https://*.ozon.ru/*
6// @match		https://seller.ozon.ru/*
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.deleteValue
10// @icon		https://st.ozone.ru/assets/favicon.ico
11// ==/UserScript==
12(function() {
13    'use strict';
14    console.log('Ozon Product Parser: Extension loaded');
15
16    // Утилита для дебаунса
17    function debounce(func, wait) {
18        let timeout;
19        return function executedFunction(...args) {
20            const later = () => {
21                clearTimeout(timeout);
22                func(...args);
23            };
24            clearTimeout(timeout);
25            timeout = setTimeout(later, wait);
26        };
27    }
28
29    // Добавляем стили
30    function addStyles() {
31        const styles = `
32            .ozon-parser-container {
33                position: fixed;
34                top: 20px;
35                right: 20px;
36                z-index: 10000;
37                display: flex;
38                gap: 10px;
39            }
40            .ozon-parser-btn {
41                background: linear-gradient(135deg, #005bff 0%, #0043c7 100%);
42                color: white;
43                border: none;
44                padding: 12px 24px;
45                border-radius: 8px;
46                cursor: pointer;
47                font-size: 14px;
48                font-weight: 600;
49                box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3);
50                transition: all 0.3s ease;
51            }
52            .ozon-parser-btn:hover {
53                background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%);
54                box-shadow: 0 6px 16px rgba(0, 91, 255, 0.4);
55                transform: translateY(-2px);
56            }
57            .ozon-parser-btn:active {
58                transform: translateY(0);
59            }
60            .ozon-parser-btn.secondary {
61                background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
62                box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
63            }
64            .ozon-parser-btn.secondary:hover {
65                background: linear-gradient(135deg, #1e7e34 0%, #155724 100%);
66                box-shadow: 0 6px 16px rgba(40, 167, 69, 0.4);
67            }
68            .ozon-parser-modal {
69                position: fixed;
70                top: 0;
71                left: 0;
72                width: 100%;
73                height: 100%;
74                background: rgba(0, 0, 0, 0.7);
75                display: flex;
76                justify-content: center;
77                align-items: center;
78                z-index: 10001;
79            }
80            .ozon-parser-modal-content {
81                background: white;
82                padding: 30px;
83                border-radius: 12px;
84                max-width: 800px;
85                width: 90%;
86                max-height: 80vh;
87                overflow-y: auto;
88                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
89            }
90            .ozon-parser-modal-header {
91                font-size: 24px;
92                font-weight: 700;
93                margin-bottom: 20px;
94                color: #333;
95            }
96            .ozon-parser-modal-body {
97                margin-bottom: 20px;
98            }
99            .ozon-parser-textarea {
100                width: 100%;
101                min-height: 200px;
102                padding: 12px;
103                border: 2px solid #e0e0e0;
104                border-radius: 8px;
105                font-size: 14px;
106                font-family: inherit;
107                resize: vertical;
108                box-sizing: border-box;
109            }
110            .ozon-parser-textarea:focus {
111                outline: none;
112                border-color: #005bff;
113            }
114            .ozon-parser-modal-footer {
115                display: flex;
116                gap: 10px;
117                justify-content: flex-end;
118            }
119            .ozon-parser-progress {
120                margin-top: 20px;
121                padding: 15px;
122                background: #f8f9fa;
123                border-radius: 8px;
124                font-size: 14px;
125                color: #333;
126            }
127            .ozon-parser-progress-bar {
128                width: 100%;
129                height: 8px;
130                background: #e0e0e0;
131                border-radius: 4px;
132                margin-top: 10px;
133                overflow: hidden;
134            }
135            .ozon-parser-progress-fill {
136                height: 100%;
137                background: linear-gradient(90deg, #005bff 0%, #0043c7 100%);
138                transition: width 0.3s ease;
139            }
140            .ozon-parser-results-table {
141                width: 100%;
142                border-collapse: collapse;
143                margin-top: 20px;
144                font-size: 13px;
145            }
146            .ozon-parser-results-table th,
147            .ozon-parser-results-table td {
148                padding: 12px;
149                text-align: left;
150                border-bottom: 1px solid #e0e0e0;
151                white-space: nowrap;
152            }
153            .ozon-parser-results-table th {
154                background: #f8f9fa;
155                font-weight: 600;
156                color: #333;
157                position: sticky;
158                top: 0;
159            }
160            .ozon-parser-results-table td:nth-child(3) {
161                max-width: 500px;
162                white-space: normal;
163                word-wrap: break-word;
164            }
165            .ozon-parser-results-table tr:hover {
166                background: #f8f9fa;
167            }
168            .ozon-parser-highlight {
169                background: #fff3cd !important;
170                font-weight: 600;
171            }
172            .ozon-parser-sku-link {
173                color: #005bff;
174                text-decoration: none;
175                font-weight: 600;
176            }
177            .ozon-parser-sku-link:hover {
178                text-decoration: underline;
179            }
180            .ozon-parser-tabs {
181                display: flex;
182                gap: 5px;
183                margin-bottom: 20px;
184                flex-wrap: wrap;
185            }
186            .ozon-parser-tab {
187                padding: 10px 20px;
188                background: #f8f9fa;
189                border: none;
190                border-radius: 6px;
191                cursor: pointer;
192                font-size: 14px;
193                transition: all 0.2s ease;
194            }
195            .ozon-parser-tab:hover {
196                background: #e9ecef;
197            }
198            .ozon-parser-tab.active {
199                background: #005bff;
200                color: white;
201                font-weight: 600;
202            }
203            .ozon-parser-info {
204                padding: 10px;
205                background: #e7f3ff;
206                border-left: 4px solid #005bff;
207                border-radius: 4px;
208                margin-bottom: 15px;
209                font-size: 13px;
210                color: #333;
211            }
212            .ozon-parser-search {
213                width: 100%;
214                padding: 10px 12px;
215                border: 2px solid #e0e0e0;
216                border-radius: 8px;
217                font-size: 14px;
218                margin-bottom: 15px;
219                box-sizing: border-box;
220            }
221            .ozon-parser-search:focus {
222                outline: none;
223                border-color: #005bff;
224            }
225            .ozon-parser-search::placeholder {
226                color: #999;
227            }
228            .ozon-parser-analytics {
229                margin-top: 30px;
230                padding: 20px;
231                background: #f8f9fa;
232                border-radius: 8px;
233            }
234            .ozon-parser-analytics-header {
235                font-size: 20px;
236                font-weight: 700;
237                margin-bottom: 20px;
238                color: #333;
239            }
240            .ozon-parser-analytics-section {
241                margin-bottom: 25px;
242            }
243            .ozon-parser-analytics-section-title {
244                font-size: 16px;
245                font-weight: 600;
246                margin-bottom: 12px;
247                color: #005bff;
248            }
249            .ozon-parser-analytics-grid {
250                display: grid;
251                grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
252                gap: 15px;
253                margin-bottom: 20px;
254            }
255            .ozon-parser-analytics-card {
256                background: white;
257                padding: 15px;
258                border-radius: 8px;
259                border: 1px solid #e0e0e0;
260            }
261            .ozon-parser-analytics-card-title {
262                font-size: 13px;
263                color: #666;
264                margin-bottom: 8px;
265            }
266            .ozon-parser-analytics-card-value {
267                font-size: 24px;
268                font-weight: 700;
269                color: #333;
270            }
271            .ozon-parser-analytics-table {
272                width: 100%;
273                border-collapse: collapse;
274                background: white;
275                border-radius: 8px;
276                overflow: hidden;
277            }
278            .ozon-parser-analytics-table th,
279            .ozon-parser-analytics-table td {
280                padding: 12px;
281                text-align: left;
282                border-bottom: 1px solid #e0e0e0;
283            }
284            .ozon-parser-analytics-table th {
285                background: #f8f9fa;
286                font-weight: 600;
287                color: #333;
288                font-size: 13px;
289            }
290            .ozon-parser-analytics-table td {
291                font-size: 13px;
292            }
293            .ozon-parser-analytics-bar {
294                height: 20px;
295                background: linear-gradient(90deg, #005bff 0%, #0043c7 100%);
296                border-radius: 4px;
297                transition: width 0.3s ease;
298            }
299        `;
300        const styleElement = document.createElement('style');
301        styleElement.textContent = styles;
302        document.head.appendChild(styleElement);
303        console.log('Ozon Product Parser: Styles added');
304    }
305
306    // Создаем UI кнопок
307    function createUI() {
308        const container = document.createElement('div');
309        container.className = 'ozon-parser-container';
310        
311        const parseBtn = document.createElement('button');
312        parseBtn.className = 'ozon-parser-btn';
313        parseBtn.textContent = 'Парсинг';
314        parseBtn.addEventListener('click', showParseModal);
315        
316        const resultsBtn = document.createElement('button');
317        resultsBtn.className = 'ozon-parser-btn secondary';
318        resultsBtn.textContent = 'Посмотреть результаты';
319        resultsBtn.addEventListener('click', showResultsModal);
320        
321        container.appendChild(parseBtn);
322        container.appendChild(resultsBtn);
323        document.body.appendChild(container);
324        console.log('Ozon Product Parser: UI created');
325    }
326
327    // Показываем модальное окно для ввода запросов
328    function showParseModal() {
329        const modal = document.createElement('div');
330        modal.className = 'ozon-parser-modal';
331        
332        const content = document.createElement('div');
333        content.className = 'ozon-parser-modal-content';
334        content.innerHTML = `
335            <div class="ozon-parser-modal-header">Парсинг товаров Ozon</div>
336            <div class="ozon-parser-modal-body">
337                <div class="ozon-parser-info">
338                    Введите поисковые запросы (каждый с новой строки). Парсер извлечет топ-16 товаров для каждого запроса.
339                </div>
340                <div style="margin-bottom: 15px;">
341                    <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Скидка Ozon (%):</label>
342                    <div style="display: flex; gap: 10px; align-items: center;">
343                        <input type="number" class="ozon-parser-search" id="ozon-discount-input" placeholder="50" value="50" min="0" max="100" step="1" style="margin-bottom: 0; flex: 1;">
344                        <button class="ozon-parser-btn" id="calculate-discount-btn" style="background: linear-gradient(135deg, #ff6b00 0%, #e65100 100%); box-shadow: 0 4px 12px rgba(255, 107, 0, 0.3); margin-bottom: 0;">Рассчитать автоматически</button>
345                    </div>
346                    <div style="font-size: 12px; color: #666; margin-top: 5px;">
347                        Укажите среднюю скидку Ozon для расчета базовой цены (цены поручения). Например, если товар продается по 500₽ со скидкой 50%, то базовая цена = 1000₽.
348                    </div>
349                </div>
350                <div style="margin-bottom: 15px;">
351                    <button class="ozon-parser-btn" id="load-list-btn" style="width: 100%; margin-bottom: 10px;">Загрузить сохраненный список</button>
352                </div>
353                <textarea class="ozon-parser-textarea" placeholder="Например:&#10;гинкго билоба&#10;аргинин&#10;витамин д"></textarea>
354                <div style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
355                    <input type="text" class="ozon-parser-search" id="list-name-input" placeholder="Название списка (например: БАДы март 2024)" style="flex: 1; margin-bottom: 0;">
356                    <button class="ozon-parser-btn secondary" id="save-list-btn">Сохранить список</button>
357                </div>
358                <div style="margin-top: 15px;">
359                    <button class="ozon-parser-btn secondary" id="manage-costs-btn" style="width: 100%;">Управление расходами (себестоимость, комиссия, доставка)</button>
360                </div>
361                <div class="ozon-parser-progress" style="display: none;">
362                    <div class="ozon-parser-progress-text">Обработка запросов...</div>
363                    <div class="ozon-parser-progress-bar">
364                        <div class="ozon-parser-progress-fill" style="width: 0%"></div>
365                    </div>
366                </div>
367            </div>
368            <div class="ozon-parser-modal-footer">
369                <button class="ozon-parser-btn" id="cancel-parse-btn">Отмена</button>
370                <button class="ozon-parser-btn" id="start-parsing-btn">Начать парсинг</button>
371            </div>
372        `;
373        
374        modal.appendChild(content);
375        document.body.appendChild(modal);
376        
377        // Закрытие по клику на фон
378        modal.addEventListener('click', (e) => {
379            if (e.target === modal) {
380                modal.remove();
381            }
382        });
383        
384        // Обработчик кнопки отмены
385        const cancelBtn = content.querySelector('#cancel-parse-btn');
386        cancelBtn.addEventListener('click', () => {
387            modal.remove();
388        });
389        
390        // Обработчик кнопки расчета скидки
391        const calculateDiscountBtn = content.querySelector('#calculate-discount-btn');
392        calculateDiscountBtn.addEventListener('click', async () => {
393            calculateDiscountBtn.disabled = true;
394            calculateDiscountBtn.textContent = 'Расчет...';
395            
396            try {
397                // Сохраняем флаг для автоматического расчета
398                await GM.setValue('ozon_parser_calculate_discount', 'true');
399                
400                // Открываем страницу в новой вкладке
401                await GM.openInTab('https://seller.ozon.ru/app/prices/control', false);
402                
403                // Ждем результата расчета
404                let attempts = 0;
405                const maxAttempts = 60; // 60 секунд максимум
406                
407                const checkInterval = setInterval(async () => {
408                    attempts++;
409                    const calculatedDiscount = await GM.getValue('ozon_parser_calculated_discount', null);
410                    const calculateFlag = await GM.getValue('ozon_parser_calculate_discount', 'false');
411                    
412                    if (calculatedDiscount !== null && calculateFlag === 'false') {
413                        // Расчет завершен
414                        clearInterval(checkInterval);
415                        
416                        // Вставляем значение в поле
417                        const discountInput = content.querySelector('#ozon-discount-input');
418                        discountInput.value = parseFloat(calculatedDiscount).toFixed(1);
419                        
420                        // Очищаем временное значение
421                        await GM.deleteValue('ozon_parser_calculated_discount');
422                        
423                        calculateDiscountBtn.disabled = false;
424                        calculateDiscountBtn.textContent = 'Рассчитать автоматически';
425                        
426                        alert(`Скидка Ozon успешно рассчитана: ${parseFloat(calculatedDiscount).toFixed(1)}%`);
427                    } else if (attempts >= maxAttempts) {
428                        // Таймаут
429                        clearInterval(checkInterval);
430                        calculateDiscountBtn.disabled = false;
431                        calculateDiscountBtn.textContent = 'Рассчитать автоматически';
432                        alert('Не удалось рассчитать скидку. Попробуйте еще раз.');
433                    }
434                }, 1000);
435            } catch (error) {
436                console.error('Ozon Product Parser: Error calculating discount:', error);
437                calculateDiscountBtn.disabled = false;
438                calculateDiscountBtn.textContent = 'Рассчитать автоматически';
439                alert('Ошибка при расчете скидки: ' + error.message);
440            }
441        });
442
443        // Обработчик кнопки сохранения списка
444        const saveListBtn = content.querySelector('#save-list-btn');
445        saveListBtn.addEventListener('click', async () => {
446            const textarea = content.querySelector('.ozon-parser-textarea');
447            const listNameInput = content.querySelector('#list-name-input');
448            const queries = textarea.value.split('\n').filter(q => q.trim());
449            const listName = listNameInput.value.trim();
450            
451            if (queries.length === 0) {
452                alert('Пожалуйста, введите хотя бы один запрос');
453                return;
454            }
455            
456            if (!listName) {
457                alert('Пожалуйста, введите название списка');
458                return;
459            }
460            
461            // Сохраняем список
462            const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
463            const savedLists = JSON.parse(savedListsJson);
464            
465            savedLists[listName] = {
466                queries: queries,
467                createdAt: new Date().toISOString(),
468                updatedAt: new Date().toISOString()
469            };
470            
471            await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
472            
473            alert(`Список "${listName}" успешно сохранен!`);
474            console.log(`Ozon Product Parser: List "${listName}" saved with ${queries.length} queries`);
475        });
476        
477        // Обработчик кнопки загрузки списка
478        const loadListBtn = content.querySelector('#load-list-btn');
479        loadListBtn.addEventListener('click', async () => {
480            await showLoadListModal(content);
481        });
482        
483        // Обработчик кнопки управления расходами
484        const manageCostsBtn = content.querySelector('#manage-costs-btn');
485        manageCostsBtn.addEventListener('click', async () => {
486            await showManageCostsModal();
487        });
488        
489        // Обработчик кнопки парсинга
490        const startBtn = content.querySelector('#start-parsing-btn');
491        startBtn.addEventListener('click', async () => {
492            const textarea = content.querySelector('.ozon-parser-textarea');
493            const listNameInput = content.querySelector('#list-name-input');
494            const ozonDiscountInput = content.querySelector('#ozon-discount-input');
495            const queries = textarea.value.split('\n').filter(q => q.trim());
496            const listName = listNameInput.value.trim() || 'Без названия';
497            const ozonDiscount = parseFloat(ozonDiscountInput.value) || 50;
498            
499            if (queries.length === 0) {
500                alert('Пожалуйста, введите хотя бы один запрос');
501                return;
502            }
503            
504            if (ozonDiscount < 0 || ozonDiscount > 100) {
505                alert('Скидка Ozon должна быть от 0 до 100%');
506                return;
507            }
508            
509            // Сохраняем скидку Ozon
510            await GM.setValue('ozon_parser_discount', ozonDiscount);
511            
512            startBtn.disabled = true;
513            startBtn.textContent = 'Парсинг...';
514            await startParsing(queries, listName, content);
515        });
516        
517        console.log('Ozon Product Parser: Parse modal shown');
518    }
519
520    // Показываем модальное окно для загрузки сохраненного списка
521    async function showLoadListModal(parentContent) {
522        const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
523        const savedLists = JSON.parse(savedListsJson);
524        const listNames = Object.keys(savedLists);
525        
526        if (listNames.length === 0) {
527            alert('Нет сохраненных списков');
528            return;
529        }
530        
531        const loadModal = document.createElement('div');
532        loadModal.className = 'ozon-parser-modal';
533        loadModal.style.zIndex = '10002';
534        
535        const loadContent = document.createElement('div');
536        loadContent.className = 'ozon-parser-modal-content';
537        loadContent.style.maxWidth = '600px';
538        
539        let listsHTML = '<div class="ozon-parser-modal-header">Выберите список</div><div class="ozon-parser-modal-body">';
540        
541        listNames.forEach(listName => {
542            const list = savedLists[listName];
543            const date = new Date(list.createdAt).toLocaleDateString('ru-RU');
544            const queriesCount = list.queries.length;
545            listsHTML += `
546                <div style="padding: 15px; background: white; border: 2px solid #e0e0e0; border-radius: 8px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; transition: all 0.2s;" 
547                     class="saved-list-item" data-list-name="${listName}">
548                    <div style="flex: 1; cursor: pointer;" class="saved-list-info">
549                        <div style="font-weight: 600; font-size: 16px; margin-bottom: 5px;">${listName}</div>
550                        <div style="font-size: 13px; color: #666;">Запросов: ${queriesCount} | Создан: ${date}</div>
551                    </div>
552                    <button class="ozon-parser-btn" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); padding: 8px 16px; font-size: 13px; margin-left: 10px;" data-delete-list="${listName}">Удалить</button>
553                </div>
554            `;
555        });
556        
557        listsHTML += '</div><div class="ozon-parser-modal-footer"><button class="ozon-parser-btn" id="close-load-modal">Отмена</button></div>';
558        
559        loadContent.innerHTML = listsHTML;
560        loadModal.appendChild(loadContent);
561        document.body.appendChild(loadModal);
562        
563        // Обработчик выбора списка (только для info блока)
564        loadContent.querySelectorAll('.saved-list-info').forEach(info => {
565            info.addEventListener('click', () => {
566                const listItem = info.closest('.saved-list-item');
567                const listName = listItem.getAttribute('data-list-name');
568                const list = savedLists[listName];
569                
570                // Заполняем textarea
571                const textarea = parentContent.querySelector('.ozon-parser-textarea');
572                const listNameInput = parentContent.querySelector('#list-name-input');
573                textarea.value = list.queries.join('\n');
574                listNameInput.value = listName;
575                
576                loadModal.remove();
577            });
578            
579            // Hover эффект
580            const listItem = info.closest('.saved-list-item');
581            info.addEventListener('mouseenter', () => {
582                listItem.style.borderColor = '#005bff';
583                listItem.style.background = '#f8f9fa';
584            });
585            info.addEventListener('mouseleave', () => {
586                listItem.style.borderColor = '#e0e0e0';
587                listItem.style.background = 'white';
588            });
589        });
590        
591        // Обработчик удаления списка
592        loadContent.querySelectorAll('[data-delete-list]').forEach(deleteBtn => {
593            deleteBtn.addEventListener('click', async (e) => {
594                e.stopPropagation();
595                const listName = deleteBtn.getAttribute('data-delete-list');
596                
597                if (!confirm(`Вы уверены, что хотите удалить список "${listName}"?`)) {
598                    return;
599                }
600                
601                // Удаляем список из savedLists
602                delete savedLists[listName];
603                await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
604                
605                // Также удаляем результаты парсинга для этого списка
606                const listResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
607                const listResults = JSON.parse(listResultsJson);
608                delete listResults[listName];
609                await GM.setValue('ozon_parser_list_results', JSON.stringify(listResults));
610                
611                console.log(`Ozon Product Parser: List "${listName}" deleted from load modal`);
612                
613                // Удаляем элемент из DOM
614                const listItem = deleteBtn.closest('.saved-list-item');
615                listItem.remove();
616                
617                // Если списков не осталось, закрываем модальное окно
618                const remainingLists = loadContent.querySelectorAll('.saved-list-item');
619                if (remainingLists.length === 0) {
620                    alert('Все списки удалены');
621                    loadModal.remove();
622                }
623            });
624        });
625        
626        // Закрытие
627        loadContent.querySelector('#close-load-modal').addEventListener('click', () => {
628            loadModal.remove();
629        });
630        
631        loadModal.addEventListener('click', (e) => {
632            if (e.target === loadModal) {
633                loadModal.remove();
634            }
635        });
636    }
637
638    // Показываем модальное окно управления расходами
639    async function showManageCostsModal() {
640        const costsJson = await GM.getValue('ozon_parser_costs', '{}');
641        const costs = JSON.parse(costsJson);
642        
643        const modal = document.createElement('div');
644        modal.className = 'ozon-parser-modal';
645        modal.style.zIndex = '10002';
646        
647        const content = document.createElement('div');
648        content.className = 'ozon-parser-modal-content';
649        content.style.maxWidth = '800px';
650        
651        content.innerHTML = `
652            <div class="ozon-parser-modal-header">Управление расходами</div>
653            <div class="ozon-parser-modal-body">
654                <div class="ozon-parser-info">
655                    Укажите расходы для ваших товаров (SKU). Эти данные будут использоваться для расчета прибыли и оптимальной цены.
656                </div>
657                
658                <!-- Блок загрузки файла -->
659                <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
660                    <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Загрузить данные из файла:</label>
661                    <div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
662                        <input type="file" id="cost-file-input" accept=".csv,.json,.txt" style="flex: 1; padding: 8px; border: 2px solid #e0e0e0; border-radius: 8px;">
663                        <button class="ozon-parser-btn" id="upload-costs-btn">Загрузить</button>
664                    </div>
665                    <details style="margin-top: 10px;">
666                        <summary style="cursor: pointer; font-size: 12px; color: #666;">Формат файлов</summary>
667                        <div style="font-size: 12px; color: #666; margin-top: 8px; line-height: 1.6;">
668                            <strong>CSV/TXT:</strong> SKU,Себестоимость,Комиссия,Доставка<br>
669                            Пример: 320244429,158.4,50,90<br><br>
670                            <strong>JSON:</strong> {"SKU": {"cost": 158.4, "commission": 0.5, "delivery": 90}}<br>
671                            Примечание: Комиссия в CSV указывается в процентах (50), в JSON - как десятичная дробь (0.5)
672                        </div>
673                    </details>
674                </div>
675                
676                <div style="margin-bottom: 15px;">
677                    <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Добавить новый товар:</label>
678                    <div style="display: grid; grid-template-columns: 2fr 1fr 1fr 1fr auto; gap: 10px; align-items: end;">
679                        <div>
680                            <label style="font-size: 12px; color: #666;">SKU</label>
681                            <input type="text" class="ozon-parser-search" id="new-sku" placeholder="320244429" style="margin-bottom: 0;">
682                        </div>
683                        <div>
684                            <label style="font-size: 12px; color: #666;">Себестоимость (₽)</label>
685                            <input type="number" class="ozon-parser-search" id="new-cost" placeholder="158.4" step="0.01" style="margin-bottom: 0;">
686                        </div>
687                        <div>
688                            <label style="font-size: 12px; color: #666;">Комиссия (%)</label>
689                            <input type="number" class="ozon-parser-search" id="new-commission" placeholder="50" step="0.1" style="margin-bottom: 0;">
690                        </div>
691                        <div>
692                            <label style="font-size: 12px; color: #666;">Доставка (₽)</label>
693                            <input type="number" class="ozon-parser-search" id="new-delivery" placeholder="90" step="0.01" style="margin-bottom: 0;">
694                        </div>
695                        <button class="ozon-parser-btn" id="add-cost-btn" style="margin-bottom: 0;">Добавить</button>
696                    </div>
697                </div>
698                <div id="costs-list" style="max-height: 400px; overflow-y: auto;">
699                    ${Object.keys(costs).length === 0 ? '<p style="text-align: center; color: #999;">Нет добавленных товаров</p>' : ''}
700                </div>
701            </div>
702            <div class="ozon-parser-modal-footer">
703                <button class="ozon-parser-btn" id="close-costs-modal">Закрыть</button>
704            </div>
705        `;
706        
707        modal.appendChild(content);
708        document.body.appendChild(modal);
709        
710        // Функция для парсинга CSV
711        function parseCSV(text) {
712            const lines = text.trim().split('\n');
713            const result = {};
714            
715            for (let i = 0; i < lines.length; i++) {
716                const line = lines[i].trim();
717                if (!line || line.startsWith('SKU')) continue; // Пропускаем заголовок
718                
719                const parts = line.split(',').map(p => p.trim());
720                if (parts.length < 4) continue;
721                
722                // Удаляем кавычки из SKU, если они есть
723                let sku = parts[0].replace(/^["']|["']$/g, '');
724                const cost = parseFloat(parts[1]);
725                const commission = parseFloat(parts[2]);
726                const delivery = parseFloat(parts[3]);
727                
728                if (sku && !isNaN(cost) && !isNaN(commission) && !isNaN(delivery)) {
729                    result[sku] = {
730                        cost: cost,
731                        commission: commission / 100, // Конвертируем проценты в десятичную дробь
732                        delivery: delivery
733                    };
734                }
735            }
736            
737            return result;
738        }
739        
740        // Обработчик загрузки файла
741        const uploadBtn = content.querySelector('#upload-costs-btn');
742        uploadBtn.addEventListener('click', async () => {
743            const fileInput = content.querySelector('#cost-file-input');
744            const file = fileInput.files[0];
745            
746            if (!file) {
747                alert('Пожалуйста, выберите файл');
748                return;
749            }
750            
751            try {
752                const text = await file.text();
753                let newCosts = {};
754                
755                if (file.name.endsWith('.json')) {
756                    // Парсим JSON
757                    try {
758                        newCosts = JSON.parse(text);
759                    } catch (jsonError) {
760                        throw new Error(`Ошибка парсинга JSON: ${jsonError.message}. Проверьте, что файл содержит корректный JSON формат.`);
761                    }
762                    
763                    // Валидация JSON формата
764                    for (const sku in newCosts) {
765                        const item = newCosts[sku];
766                        if (typeof item.cost !== 'number' || typeof item.commission !== 'number' || typeof item.delivery !== 'number') {
767                            throw new Error(`Неверный формат данных для SKU ${sku}. Ожидается: {"cost": число, "commission": число, "delivery": число}`);
768                        }
769                    }
770                } else {
771                    // Парсим CSV/TXT
772                    newCosts = parseCSV(text);
773                }
774                
775                if (Object.keys(newCosts).length === 0) {
776                    alert('Не удалось извлечь данные из файла. Проверьте формат.');
777                    return;
778                }
779                
780                // Объединяем с существующими данными
781                const mergedCosts = { ...costs, ...newCosts };
782                await GM.setValue('ozon_parser_costs', JSON.stringify(mergedCosts));
783                
784                // Обновляем costs переменную
785                Object.assign(costs, newCosts);
786                
787                alert(`Успешно загружено ${Object.keys(newCosts).length} товаров`);
788                displayCostsList();
789                
790                // Очищаем input
791                fileInput.value = '';
792            } catch (error) {
793                console.error('Ozon Product Parser: Error uploading costs file:', error);
794                alert('Ошибка при загрузке файла: ' + error.message);
795            }
796        });
797        
798        // Функция для отображения списка расходов
799        function displayCostsList() {
800            const costsList = content.querySelector('#costs-list');
801            
802            if (Object.keys(costs).length === 0) {
803                costsList.innerHTML = '<p style="text-align: center; color: #999;">Нет добавленных товаров</p>';
804                return;
805            }
806            
807            let html = '<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">';
808            html += '<div style="font-size: 14px; font-weight: 600;">Всего товаров: ' + Object.keys(costs).length + '</div>';
809            html += '<button class="ozon-parser-btn" id="delete-all-costs-btn" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); padding: 8px 16px; font-size: 13px;">Удалить все расходы</button>';
810            html += '</div>';
811            html += '<table class="ozon-parser-results-table"><thead><tr><th>SKU</th><th>Себестоимость</th><th>Комиссия</th><th>Доставка</th><th>Действия</th></tr></thead><tbody>';
812            
813            Object.keys(costs).forEach(sku => {
814                const cost = costs[sku];
815                html += `
816                    <tr>
817                        <td><a href="https://www.ozon.ru/product/${sku}" target="_blank" class="ozon-parser-sku-link">${sku}</a></td>
818                        <td>${cost.cost.toFixed(2)} ₽</td>
819                        <td>${(cost.commission * 100).toFixed(2)}%</td>
820                        <td>${cost.delivery.toFixed(2)} ₽</td>
821                        <td>
822                            <button class="ozon-parser-btn" data-edit-sku="${sku}" style="padding: 6px 12px; font-size: 12px; margin-right: 5px;">Изменить</button>
823                            <button class="ozon-parser-btn" data-delete-sku="${sku}" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); padding: 6px 12px; font-size: 12px;">Удалить</button>
824                        </td>
825                    </tr>
826                `;
827            });
828            
829            html += '</tbody></table>';
830            costsList.innerHTML = html;
831            
832            // Обработчик для кнопки удаления всех расходов
833            const deleteAllBtn = costsList.querySelector('#delete-all-costs-btn');
834            if (deleteAllBtn) {
835                deleteAllBtn.addEventListener('click', async () => {
836                    if (confirm('Вы уверены, что хотите удалить ВСЕ данные о расходах? Это действие нельзя отменить.')) {
837                        // Очищаем все данные
838                        Object.keys(costs).forEach(key => delete costs[key]);
839                        await GM.setValue('ozon_parser_costs', JSON.stringify({}));
840                        displayCostsList();
841                        alert('Все данные о расходах удалены');
842                    }
843                });
844            }
845            
846            // Обработчики для кнопок удаления
847            costsList.querySelectorAll('[data-delete-sku]').forEach(btn => {
848                btn.addEventListener('click', async () => {
849                    const sku = btn.getAttribute('data-delete-sku');
850                    if (confirm(`Удалить данные о расходах для SKU ${sku}?`)) {
851                        delete costs[sku];
852                        await GM.setValue('ozon_parser_costs', JSON.stringify(costs));
853                        displayCostsList();
854                    }
855                });
856            });
857            
858            // Обработчики для кнопок изменения
859            costsList.querySelectorAll('[data-edit-sku]').forEach(btn => {
860                btn.addEventListener('click', () => {
861                    const sku = btn.getAttribute('data-edit-sku');
862                    const cost = costs[sku];
863                    
864                    content.querySelector('#new-sku').value = sku;
865                    content.querySelector('#new-cost').value = cost.cost;
866                    content.querySelector('#new-commission').value = cost.commission * 100;
867                    content.querySelector('#new-delivery').value = cost.delivery;
868                    
869                    content.querySelector('#new-sku').scrollIntoView({ behavior: 'smooth', block: 'center' });
870                });
871            });
872        }
873        
874        displayCostsList();
875        
876        // Обработчик добавления нового товара
877        const addBtn = content.querySelector('#add-cost-btn');
878        addBtn.addEventListener('click', async () => {
879            const sku = content.querySelector('#new-sku').value.trim();
880            const cost = parseFloat(content.querySelector('#new-cost').value);
881            const commission = parseFloat(content.querySelector('#new-commission').value);
882            const delivery = parseFloat(content.querySelector('#new-delivery').value);
883            
884            if (!sku) {
885                alert('Пожалуйста, введите SKU');
886                return;
887            }
888            
889            if (isNaN(cost) || cost < 0) {
890                alert('Пожалуйста, введите корректную себестоимость');
891                return;
892            }
893            
894            if (isNaN(commission) || commission < 0 || commission > 100) {
895                alert('Пожалуйста, введите корректную комиссию (0-100%)');
896                return;
897            }
898            
899            if (isNaN(delivery) || delivery < 0) {
900                alert('Пожалуйста, введите корректную стоимость доставки');
901                return;
902            }
903            
904            costs[sku] = {
905                cost: cost,
906                commission: commission / 100, // Сохраняем как десятичную дробь
907                delivery: delivery
908            };
909            
910            await GM.setValue('ozon_parser_costs', JSON.stringify(costs));
911            
912            // Очищаем поля
913            content.querySelector('#new-sku').value = '';
914            content.querySelector('#new-cost').value = '';
915            content.querySelector('#new-commission').value = '';
916            content.querySelector('#new-delivery').value = '';
917            
918            displayCostsList();
919        });
920        
921        // Закрытие
922        content.querySelector('#close-costs-modal').addEventListener('click', () => {
923            modal.remove();
924        });
925        
926        modal.addEventListener('click', (e) => {
927            if (e.target === modal) {
928                modal.remove();
929            }
930        });
931    }
932
933    // Рассчитываем скидку Ozon
934    async function calculateOzonDiscount() {
935        console.log('Ozon Product Parser: Starting Ozon discount calculation');
936        
937        // Проверяем, находимся ли мы уже на странице управления ценами
938        if (window.location.href.includes('seller.ozon.ru/app/prices/control')) {
939            // Уже на нужной странице, извлекаем данные
940            await extractDiscountFromPage();
941        } else {
942            // Переходим на страницу управления ценами
943            await GM.setValue('ozon_parser_calculate_discount', 'true');
944            window.location.href = 'https://seller.ozon.ru/app/prices/control';
945        }
946    }
947
948    // Извлекаем скидку со страницы управления ценами
949    async function extractDiscountFromPage() {
950        console.log('Ozon Product Parser: Extracting discount from prices page');
951        
952        // Ждем загрузки таблицы с ценами
953        await waitForPricesTable();
954        
955        // Прокручиваем страницу вниз для загрузки цен (ленивая загрузка)
956        console.log('Ozon Product Parser: Scrolling to load prices');
957        for (let i = 0; i < 3; i++) {
958            window.scrollBy(0, 500);
959            await new Promise(resolve => setTimeout(resolve, 2000));
960        }
961        
962        try {
963            // Ищем цены по правильным классам
964            // Цена продажи (с картой Ozon): index_priceByOzonCardCurrency_3DLKf
965            // Базовая цена (цена поручения): index_priceAmount_3dfpL
966            
967            const salePriceElements = document.querySelectorAll('.index_priceByOzonCardCurrency_3DLKf');
968            const basePriceElements = document.querySelectorAll('.index_priceAmount_3dfpL');
969            
970            console.log(`Ozon Product Parser: Found ${salePriceElements.length} sale prices and ${basePriceElements.length} base prices`);
971            
972            if (salePriceElements.length === 0 || basePriceElements.length === 0) {
973                console.error('Ozon Product Parser: Could not find price elements on the page');
974                alert('Не удалось найти цены на странице. Убедитесь, что у вас есть товары с ценами.');
975                await GM.setValue('ozon_parser_calculate_discount', 'false');
976                return;
977            }
978            
979            // Собираем пары цен (базовая и продажная)
980            let validDiscounts = [];
981            
982            // Ищем строки таблицы с обеими ценами
983            const rows = document.querySelectorAll('tr');
984            for (const row of rows) {
985                const salePrice = row.querySelector('.index_priceByOzonCardCurrency_3DLKf');
986                const basePrice = row.querySelector('.index_priceAmount_3dfpL');
987                
988                if (salePrice && basePrice) {
989                    const salePriceText = salePrice.textContent.trim();
990                    const basePriceText = basePrice.textContent.trim();
991                    
992                    // Парсим цены (убираем пробелы и символ рубля)
993                    const salePriceValue = parseFloat(salePriceText.replace(/\s/g, '').replace('₽', '').replace(',', '.'));
994                    const basePriceValue = parseFloat(basePriceText.replace(/\s/g, '').replace('₽', '').replace(',', '.'));
995                    
996                    if (!isNaN(salePriceValue) && !isNaN(basePriceValue) && basePriceValue > 0 && salePriceValue > 0) {
997                        // Рассчитываем скидку: (базовая цена - цена продажи) / базовая цена * 100
998                        const discountAmount = basePriceValue - salePriceValue;
999                        const discountPercent = (discountAmount / basePriceValue) * 100;
1000                        
1001                        if (discountPercent > 0 && discountPercent < 100) {
1002                            validDiscounts.push({
1003                                basePrice: basePriceValue,
1004                                salePrice: salePriceValue,
1005                                discount: discountPercent
1006                            });
1007                            
1008                            console.log(`Ozon Product Parser: Base price: ${basePriceValue}₽, Sale price: ${salePriceValue}₽, Discount: ${discountPercent.toFixed(1)}%`);
1009                        }
1010                    }
1011                }
1012            }
1013            
1014            if (validDiscounts.length === 0) {
1015                console.error('Ozon Product Parser: No valid price pairs found');
1016                alert('Не удалось найти валидные пары цен на странице.');
1017                await GM.setValue('ozon_parser_calculate_discount', 'false');
1018                return;
1019            }
1020            
1021            // Рассчитываем среднюю скидку
1022            const avgDiscount = validDiscounts.reduce((sum, item) => sum + item.discount, 0) / validDiscounts.length;
1023            
1024            console.log(`Ozon Product Parser: Calculated average discount from ${validDiscounts.length} products: ${avgDiscount.toFixed(1)}%`);
1025            
1026            // Сохраняем рассчитанную скидку для передачи в основное окно
1027            await GM.setValue('ozon_parser_calculated_discount', avgDiscount);
1028            
1029            // Сбрасываем флаг расчета
1030            await GM.setValue('ozon_parser_calculate_discount', 'false');
1031            
1032            // Закрываем текущую вкладку
1033            window.close();
1034        } catch (error) {
1035            console.error('Ozon Product Parser: Error extracting discount:', error);
1036            alert('Ошибка при извлечении данных о скидке: ' + error.message);
1037            
1038            // Сбрасываем флаг расчета
1039            await GM.setValue('ozon_parser_calculate_discount', 'false');
1040        }
1041    }
1042
1043    // Ждем появления таблицы с ценами
1044    function waitForPricesTable() {
1045        return new Promise((resolve) => {
1046            const checkTable = () => {
1047                const rows = document.querySelectorAll('tr');
1048                if (rows.length > 0) {
1049                    console.log('Ozon Product Parser: Prices table found');
1050                    // Дополнительная задержка для полной загрузки данных
1051                    setTimeout(resolve, 3000);
1052                } else {
1053                    setTimeout(checkTable, 1000);
1054                }
1055            };
1056            checkTable();
1057        });
1058    }
1059
1060    // Прокручиваем страницу для подгрузки товаров
1061    async function scrollToLoadProducts(targetCount = 20) {
1062        console.log(`Ozon Product Parser: Scrolling to load ${targetCount} products`);
1063        
1064        const table = document.querySelector('#mpstat-ozone-search-result table');
1065        if (!table) {
1066            console.error('Ozon Product Parser: Table not found for scrolling');
1067            return;
1068        }
1069        
1070        let previousRowCount = 0;
1071        let attempts = 0;
1072        const maxAttempts = 10;
1073        
1074        while (attempts < maxAttempts) {
1075            const rows = table.querySelectorAll('tbody tr');
1076            const currentRowCount = rows.length;
1077            
1078            console.log(`Ozon Product Parser: Current row count: ${currentRowCount}, target: ${targetCount}`);
1079            
1080            if (currentRowCount >= targetCount) {
1081                console.log(`Ozon Product Parser: Loaded ${currentRowCount} products`);
1082                break;
1083            }
1084            
1085            // Если количество строк не изменилось, значит больше товаров нет
1086            if (currentRowCount === previousRowCount && attempts > 2) {
1087                console.log(`Ozon Product Parser: No more products to load (${currentRowCount} total)`);
1088                break;
1089            }
1090            
1091            previousRowCount = currentRowCount;
1092            
1093            // Прокручиваем к последней строке таблицы
1094            const lastRow = rows[rows.length - 1];
1095            if (lastRow) {
1096                lastRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
1097            }
1098            
1099            // Также прокручиваем окно вниз
1100            window.scrollBy(0, 500);
1101            
1102            // Ждем подгрузки новых товаров
1103            await new Promise(resolve => setTimeout(resolve, 2000));
1104            attempts++;
1105        }
1106        
1107        console.log(`Ozon Product Parser: Scrolling completed after ${attempts} attempts`);
1108    }
1109
1110    // Начинаем парсинг
1111    async function startParsing(queries, listName, modalContent) {
1112        const progressDiv = modalContent.querySelector('.ozon-parser-progress');
1113        progressDiv.style.display = 'block';
1114        
1115        // Сохраняем список запросов и название списка
1116        await GM.setValue('ozon_parser_queries', JSON.stringify(queries));
1117        await GM.setValue('ozon_parser_current_list_name', listName);
1118        await GM.setValue('ozon_parser_current_index', 0);
1119        await GM.setValue('ozon_parser_active', 'true');
1120        console.log(`Ozon Product Parser: Starting parsing process for list "${listName}"`);
1121        
1122        // Переходим к первому запросу
1123        const firstQuery = queries[0].trim();
1124        const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(firstQuery)}&from_global=true`;
1125        window.location.href = searchUrl;
1126    }
1127
1128    // Продолжаем парсинг после загрузки страницы
1129    async function continueParsingIfActive() {
1130        const isActive = await GM.getValue('ozon_parser_active', 'false');
1131        if (isActive !== 'true') {
1132            return;
1133        }
1134        
1135        console.log('Ozon Product Parser: Continuing parsing process');
1136        
1137        // Ждем появления таблицы
1138        await waitForTable();
1139        
1140        // Получаем текущее состояние
1141        const queriesJson = await GM.getValue('ozon_parser_queries', '[]');
1142        const queries = JSON.parse(queriesJson);
1143        const currentIndex = await GM.getValue('ozon_parser_current_index', 0);
1144        const currentListName = await GM.getValue('ozon_parser_current_list_name', 'Без названия');
1145        
1146        // Получаем результаты для текущего списка
1147        const allListResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
1148        const allListResults = JSON.parse(allListResultsJson);
1149        
1150        if (!allListResults[currentListName]) {
1151            allListResults[currentListName] = {
1152                queries: {},
1153                createdAt: new Date().toISOString(),
1154                updatedAt: new Date().toISOString()
1155            };
1156        }
1157        
1158        if (currentIndex >= queries.length) {
1159            // Парсинг завершен
1160            await GM.setValue('ozon_parser_active', 'false');
1161            
1162            // Обновляем дату последнего обновления списка
1163            allListResults[currentListName].updatedAt = new Date().toISOString();
1164            await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
1165            
1166            console.log('Ozon Product Parser: Parsing completed');
1167            return;
1168        }
1169        
1170        const currentQuery = queries[currentIndex].trim();
1171        console.log(`Ozon Product Parser: Processing query ${currentIndex + 1}/${queries.length}: "${currentQuery}"`);
1172        
1173        // Парсим данные текущей страницы
1174        const products = await parseProducts();
1175        allListResults[currentListName].queries[currentQuery] = products;
1176        
1177        // Сохраняем результаты
1178        await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
1179        console.log(`Ozon Product Parser: Parsed ${products.length} products for "${currentQuery}" in list "${currentListName}"`);
1180        
1181        // Переходим к следующему запросу
1182        const nextIndex = currentIndex + 1;
1183        await GM.setValue('ozon_parser_current_index', nextIndex);
1184        
1185        if (nextIndex < queries.length) {
1186            // Есть еще запросы - переходим к следующему
1187            const nextQuery = queries[nextIndex].trim();
1188            const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(nextQuery)}&from_global=true`;
1189            setTimeout(() => {
1190                window.location.href = searchUrl;
1191            }, 2000); // Небольшая задержка между запросами
1192        } else {
1193            // Все запросы обработаны
1194            await GM.setValue('ozon_parser_active', 'false');
1195            
1196            // Обновляем дату последнего обновления списка
1197            allListResults[currentListName].updatedAt = new Date().toISOString();
1198            await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
1199            
1200            console.log('Ozon Product Parser: All queries processed');
1201            
1202            // Показываем результаты
1203            setTimeout(() => {
1204                showResultsModal();
1205            }, 1000);
1206        }
1207    }
1208
1209    // Продолжаем расчет скидки после загрузки страницы
1210    async function continueDiscountCalculationIfActive() {
1211        const shouldCalculate = await GM.getValue('ozon_parser_calculate_discount', 'false');
1212        if (shouldCalculate === 'true') {
1213            await GM.setValue('ozon_parser_calculate_discount', 'false');
1214            console.log('Ozon Product Parser: Continuing discount calculation');
1215            await extractDiscountFromPage();
1216        }
1217    }
1218
1219    // Ждем появления таблицы
1220    function waitForTable() {
1221        return new Promise((resolve) => {
1222            const checkTable = () => {
1223                const table = document.querySelector('#mpstat-ozone-search-result table tbody');
1224                if (table && table.querySelectorAll('tr').length > 0) {
1225                    console.log('Ozon Product Parser: Table found');
1226                    // Дополнительная задержка для полной загрузки данных
1227                    setTimeout(resolve, 5000);
1228                } else {
1229                    setTimeout(checkTable, 1000);
1230                }
1231            };
1232            checkTable();
1233        });
1234    }
1235
1236    // Прокручиваем страницу для подгрузки товаров
1237    async function scrollToLoadProducts(targetCount = 20) {
1238        console.log(`Ozon Product Parser: Scrolling to load ${targetCount} products`);
1239        
1240        const table = document.querySelector('#mpstat-ozone-search-result table');
1241        if (!table) {
1242            console.error('Ozon Product Parser: Table not found for scrolling');
1243            return;
1244        }
1245        
1246        let previousRowCount = 0;
1247        let attempts = 0;
1248        const maxAttempts = 10;
1249        
1250        while (attempts < maxAttempts) {
1251            const rows = table.querySelectorAll('tbody tr');
1252            const currentRowCount = rows.length;
1253            
1254            console.log(`Ozon Product Parser: Current row count: ${currentRowCount}, target: ${targetCount}`);
1255            
1256            if (currentRowCount >= targetCount) {
1257                console.log(`Ozon Product Parser: Loaded ${currentRowCount} products`);
1258                break;
1259            }
1260            
1261            // Если количество строк не изменилось, значит больше товаров нет
1262            if (currentRowCount === previousRowCount && attempts > 2) {
1263                console.log(`Ozon Product Parser: No more products to load (${currentRowCount} total)`);
1264                break;
1265            }
1266            
1267            previousRowCount = currentRowCount;
1268            
1269            // Прокручиваем к последней строке таблицы
1270            const lastRow = rows[rows.length - 1];
1271            if (lastRow) {
1272                lastRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
1273            }
1274            
1275            // Также прокручиваем окно вниз
1276            window.scrollBy(0, 500);
1277            
1278            // Ждем подгрузки новых товаров
1279            await new Promise(resolve => setTimeout(resolve, 2000));
1280            attempts++;
1281        }
1282        
1283        console.log(`Ozon Product Parser: Scrolling completed after ${attempts} attempts`);
1284    }
1285
1286    // Парсим товары из таблицы
1287    async function parseProducts() {
1288        const table = document.querySelector('#mpstat-ozone-search-result table');
1289        if (!table) {
1290            console.error('Ozon Product Parser: Table not found');
1291            return [];
1292        }
1293        
1294        // Прокручиваем страницу для подгрузки товаров
1295        await scrollToLoadProducts(20);
1296        
1297        const rows = table.querySelectorAll('tbody tr');
1298        const products = [];
1299        const maxProducts = Math.min(20, rows.length);
1300        
1301        // Получаем названия товаров из карточек на странице
1302        const productLinks = document.querySelectorAll('a[href*="/product/"]');
1303        const productNames = new Map();
1304        
1305        console.log(`Ozon Product Parser: Found ${productLinks.length} product links`);
1306        
1307        productLinks.forEach(link => {
1308            const href = link.getAttribute('href');
1309            const skuMatch = href.match(/\/product\/[^/]+-(\d+)/);
1310            if (skuMatch) {
1311                const sku = skuMatch[1];
1312                // Ищем название в родительском элементе
1313                const parent = link.closest('.tile-root') || link.closest('[data-index]');
1314                if (parent) {
1315                    const nameElement = parent.querySelector('.tsBody500Medium');
1316                    if (nameElement && nameElement.textContent.trim().length > 10) {
1317                        productNames.set(sku, nameElement.textContent.trim());
1318                    }
1319                }
1320            }
1321        });
1322        
1323        console.log(`Ozon Product Parser: Extracted ${productNames.size} product names`);
1324        
1325        // Функция для извлечения количества единиц из названия
1326        function extractQuantity(name) {
1327            if (!name) return null;
1328            
1329            // Ищем паттерны: "120 капсул", "60 таблеток", "180шт", "90 шт"
1330            const patterns = [
1331                /(\d+)\s*(?:капсул|капс|caps)/i,
1332                /(\d+)\s*(?:таблеток|табл|tablets|tabs)/i,
1333                /(\d+)\s*(?:штук|шт|pcs|pieces)/i,
1334                /(\d+)\s*(?:порций|servings)/i
1335            ];
1336            
1337            for (const pattern of patterns) {
1338                const match = name.match(pattern);
1339                if (match) {
1340                    const quantity = parseInt(match[1]);
1341                    if (quantity > 0 && quantity <= 1000) { // Разумные пределы
1342                        return quantity;
1343                    }
1344                }
1345            }
1346            
1347            return null;
1348        }
1349        
1350        for (let i = 0; i < maxProducts; i++) {
1351            const row = rows[i];
1352            const cells = row.querySelectorAll('td');
1353            if (cells.length < 8) continue;
1354            
1355            const position = cells[0]?.textContent.trim() || '';
1356            const sku = cells[2]?.textContent.trim() || '';
1357            const brand = cells[3]?.textContent.trim() || '';
1358            const priceText = cells[4]?.textContent.trim() || '';
1359            const revenueText = cells[6]?.textContent.trim() || '';
1360            const ordersText = cells[7]?.textContent.trim() || '';
1361            
1362            // Получаем название товара из карточки
1363            const name = productNames.get(sku) || '';
1364            
1365            // Извлекаем количество единиц
1366            const quantity = extractQuantity(name);
1367            
1368            // Проверяем, есть ли товар от GLS или Skinphoria
1369            const isTargetBrand = brand.includes('GLS Pharmaceuticals') || brand.includes('Skinphoria');
1370            
1371            // Парсим числовые значения
1372            const price = parseFloat(priceText.replace(/[^\d]/g, '')) || 0;
1373            const revenue = parseFloat(revenueText.replace(/[^\d]/g, '')) || 0;
1374            const orders = parseInt(ordersText.replace(/[^\d]/g, '')) || 0;
1375            
1376            // Рассчитываем цену за единицу
1377            const pricePerUnit = quantity && price > 0 ? price / quantity : null;
1378            
1379            products.push({
1380                position: parseInt(position) || (i + 1),
1381                sku,
1382                name,
1383                brand,
1384                price,
1385                revenue,
1386                orders,
1387                isTargetBrand,
1388                quantity,
1389                pricePerUnit
1390            });
1391        }
1392        
1393        // Сортируем по убыванию выручки
1394        products.sort((a, b) => b.revenue - a.revenue);
1395        console.log(`Ozon Product Parser: Parsed ${products.length} products, ${products.filter(p => p.isTargetBrand).length} from target brands`);
1396        return products;
1397    }
1398
1399    // Анализируем данные для запроса
1400    async function analyzeProducts(products) {
1401        console.log('Ozon Product Parser: Starting analysis for', products.length, 'products');
1402        
1403        if (!products || products.length === 0) {
1404            console.error('Ozon Product Parser: No products to analyze');
1405            return null;
1406        }
1407
1408        try {
1409            // Получаем данные о расходах и скидке Ozon
1410            const costsJson = await GM.getValue('ozon_parser_costs', '{}');
1411            const costs = JSON.parse(costsJson);
1412            const ozonDiscount = await GM.getValue('ozon_parser_discount', 50);
1413            console.log(`Ozon Product Parser: Loaded costs for ${Object.keys(costs).length} SKUs, Ozon discount: ${ozonDiscount}%`);
1414
1415            // Общая выручка и заказы
1416            const totalRevenue = products.reduce((sum, p) => sum + p.revenue, 0);
1417            const totalOrders = products.reduce((sum, p) => sum + p.orders, 0);
1418
1419            // Топ-5 товаров по выручке
1420            const topProducts = products.slice(0, 5).map(p => ({
1421                name: p.name,
1422                brand: p.brand,
1423                price: p.price,
1424                revenue: p.revenue,
1425                orders: p.orders,
1426                position: p.position,
1427                revenueShare: ((p.revenue / totalRevenue) * 100).toFixed(1)
1428            }));
1429
1430            // Ценовые сегменты
1431            const prices = products.map(p => p.price).filter(p => p > 0);
1432            const minPrice = Math.min(...prices);
1433            const maxPrice = Math.max(...prices);
1434            const priceRange = maxPrice - minPrice;
1435            const segmentSize = priceRange / 4;
1436
1437            const priceSegments = [
1438                { min: minPrice, max: minPrice + segmentSize, name: 'Низкий' },
1439                { min: minPrice + segmentSize, max: minPrice + segmentSize * 2, name: 'Средний-' },
1440                { min: minPrice + segmentSize * 2, max: minPrice + segmentSize * 3, name: 'Средний+' },
1441                { min: minPrice + segmentSize * 3, max: maxPrice, name: 'Высокий' }
1442            ];
1443
1444            const segments = priceSegments.map(segment => {
1445                const segmentProducts = products.filter(p => 
1446                    p.price >= segment.min && p.price <= segment.max
1447                );
1448                const segmentRevenue = segmentProducts.reduce((sum, p) => sum + p.revenue, 0);
1449                const segmentOrders = segmentProducts.reduce((sum, p) => sum + p.orders, 0);
1450
1451                return {
1452                    name: segment.name,
1453                    priceRange: `${Math.round(segment.min)} - ${Math.round(segment.max)}`,
1454                    count: segmentProducts.length,
1455                    revenue: segmentRevenue,
1456                    orders: segmentOrders,
1457                    revenueShare: ((segmentRevenue / totalRevenue) * 100).toFixed(1),
1458                    avgPrice: segmentProducts.length > 0 
1459                        ? Math.round(segmentProducts.reduce((sum, p) => sum + p.price, 0) / segmentProducts.length)
1460                        : 0
1461                };
1462            }).filter(s => s.count > 0);
1463
1464            // Расчет эластичности запроса
1465            const competitors = products.filter(p => !p.isTargetBrand);
1466            let elasticity = null;
1467            let elasticityInterpretation = '';
1468            
1469            if (competitors.length >= 5) {
1470                const logPrices = competitors.map(p => Math.log(p.price + 1));
1471                const logOrders = competitors.map(p => Math.log(p.orders + 1));
1472                
1473                function simpleRegression(x, y) {
1474                    const n = x.length;
1475                    const sumX = x.reduce((a, b) => a + b, 0);
1476                    const sumY = y.reduce((a, b) => a + b, 0);
1477                    const sumXY = x.reduce((a, b, i) => a + b * y[i], 0);
1478                    const sumX2 = x.reduce((a, b) => a + b * b, 0);
1479                    const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
1480                    return slope;
1481                }
1482                
1483                const rawElasticity = simpleRegression(logPrices, logOrders);
1484                const priorElasticity = -1.2;
1485                const priorWeight = 0.2;
1486                elasticity = rawElasticity * (1 - priorWeight) + priorElasticity * priorWeight;
1487                
1488                console.log(`Ozon Product Parser: Raw elasticity: ${rawElasticity.toFixed(2)}, Bayesian adjusted: ${elasticity.toFixed(2)}`);
1489                
1490                if (elasticity < -1.5) {
1491                    elasticityInterpretation = 'Высокая эластичность - спрос очень чувствителен к цене. Снижение цены сильно увеличит продажи.';
1492                } else if (elasticity < -0.8) {
1493                    elasticityInterpretation = 'Средняя эластичность - спрос умеренно реагирует на изменение цены.';
1494                } else if (elasticity < 0) {
1495                    elasticityInterpretation = 'Низкая эластичность - спрос слабо зависит от цены. Можно повышать цену.';
1496                } else {
1497                    elasticityInterpretation = 'Аномальная эластичность - возможно недостаточно данных для анализа.';
1498                }
1499            }
1500
1501            // Функция для расчета прогноза
1502            function calculateForecast(newPrice, currentPrice, currentOrders, currentRevenue, productCosts) {
1503                const priceChange = (newPrice - currentPrice) / currentPrice;
1504                const usedElasticity = elasticity !== null ? elasticity : -1.2;
1505                const ordersChange = usedElasticity * priceChange;
1506                
1507                const forecastOrders = Math.round(currentOrders * (1 + ordersChange));
1508                const forecastRevenue = Math.round(forecastOrders * newPrice);
1509                
1510                let forecastProfit = null;
1511                let currentProfit = null;
1512                let profitChange = null;
1513                
1514                if (productCosts) {
1515                    const basePrice = newPrice / (1 - ozonDiscount / 100);
1516                    const sellerRevenue = basePrice * (1 - productCosts.commission) * forecastOrders;
1517                    const expenses = (productCosts.cost + productCosts.delivery) * forecastOrders;
1518                    forecastProfit = sellerRevenue - expenses;
1519                    
1520                    const currentBasePrice = currentPrice / (1 - ozonDiscount / 100);
1521                    const currentSellerRevenue = currentBasePrice * (1 - productCosts.commission) * currentOrders;
1522                    const currentExpenses = (productCosts.cost + productCosts.delivery) * currentOrders;
1523                    currentProfit = currentSellerRevenue - currentExpenses;
1524                    
1525                    profitChange = currentProfit > 0 ? Math.round(((forecastProfit - currentProfit) / currentProfit) * 100) : 0;
1526                }
1527                
1528                return {
1529                    orders: forecastOrders,
1530                    ordersChange: Math.round(ordersChange * 100),
1531                    revenue: forecastRevenue,
1532                    revenueChange: Math.round(((forecastRevenue - currentRevenue) / currentRevenue) * 100),
1533                    profit: forecastProfit,
1534                    profitChange: profitChange
1535                };
1536            }
1537
1538            // Функция для расчета рекомендованной цены для конкретного товара
1539            function calculatePriceForProduct(targetProduct, allProducts) {
1540                console.log(`Calculating price for SKU ${targetProduct.sku}`);
1541                
1542                const productCosts = costs[targetProduct.sku];
1543                
1544                // Находим конкурентов
1545                const positionRange = 5;
1546                const nearbyProducts = allProducts.filter(p => 
1547                    !p.isTargetBrand && 
1548                    Math.abs(p.position - targetProduct.position) <= positionRange &&
1549                    p.price > 0 && p.orders > 0
1550                );
1551                
1552                const referenceProducts = nearbyProducts.length >= 3 
1553                    ? nearbyProducts 
1554                    : allProducts.filter(p => !p.isTargetBrand && p.price > 0 && p.orders > 0).slice(0, 10);
1555                
1556                if (referenceProducts.length === 0) {
1557                    return null;
1558                }
1559                
1560                // Рассчитываем базовую оптимальную цену на основе конкурентов
1561                let weightedSum = 0;
1562                let weightSum = 0;
1563                referenceProducts.forEach(p => {
1564                    const weight = p.revenue;
1565                    weightedSum += p.price * weight;
1566                    weightSum += weight;
1567                });
1568                const weightedPrice = weightSum > 0 ? weightedSum / weightSum : 0;
1569                
1570                const productsWithConversion = referenceProducts.map(p => ({
1571                    ...p,
1572                    conversion: p.orders / p.price
1573                })).sort((a, b) => b.conversion - a.conversion);
1574                const topConversionPrice = productsWithConversion.length > 0 
1575                    ? productsWithConversion.slice(0, 3).reduce((sum, p) => sum + p.price, 0) / 3
1576                    : 0;
1577                
1578                const top10Prices = referenceProducts.slice(0, 10).map(p => p.price).sort((a, b) => a - b);
1579                const medianPrice = top10Prices.length > 0 
1580                    ? top10Prices[Math.floor(top10Prices.length / 2)]
1581                    : 0;
1582
1583                const marketOptimalPrice = (weightedPrice * 0.4 + topConversionPrice * 0.3 + medianPrice * 0.3);
1584                
1585                // Рассчитываем RPI
1586                const competitorPrices = allProducts.filter(p => !p.isTargetBrand && p.price > 0);
1587                let avgCompetitorPrice = 0;
1588                if (competitorPrices.length > 0) {
1589                    let weightedPriceSum = 0;
1590                    let weightSum = 0;
1591                    competitorPrices.forEach(comp => {
1592                        const weight = comp.revenue;
1593                        weightedPriceSum += comp.price * weight;
1594                        weightSum += weight;
1595                    });
1596                    avgCompetitorPrice = weightSum > 0 ? weightedPriceSum / weightSum : targetProduct.price;
1597                }
1598                
1599                const rpi = avgCompetitorPrice > 0 ? (targetProduct.price / avgCompetitorPrice) * 100 : 100;
1600                
1601                // Используем эластичность для оптимизации прибыли
1602                const usedElasticity = elasticity !== null ? elasticity : -1.2;
1603                
1604                // Функция для расчета ожидаемой прибыли при заданной цене
1605                function calculateExpectedProfit(price) {
1606                    if (!productCosts) return null;
1607                    
1608                    const priceChange = (price - targetProduct.price) / targetProduct.price;
1609                    const ordersChange = usedElasticity * priceChange;
1610                    const expectedOrders = Math.max(1, targetProduct.orders * (1 + ordersChange));
1611                    
1612                    const basePrice = price / (1 - ozonDiscount / 100);
1613                    const sellerRevenue = basePrice * (1 - productCosts.commission) * expectedOrders;
1614                    const expenses = (productCosts.cost + productCosts.delivery) * expectedOrders;
1615                    
1616                    return sellerRevenue - expenses;
1617                }
1618                
1619                // Рассчитываем 4 варианта цен с учетом эластичности и максимизации прибыли
1620                // Захват рынка: ищем оптимальную цену ниже текущей для РОСТА прибыли
1621                // Цена ОБЯЗАТЕЛЬНО ниже текущей, но прибыль должна РАСТИ за счет увеличения объема
1622                let marketCapturePrice = targetProduct.price;
1623                if (productCosts) {
1624                    const currentProfit = calculateExpectedProfit(targetProduct.price);
1625                    let bestPrice = targetProduct.price;
1626                    let maxProfit = currentProfit;
1627                    
1628                    // Ищем оптимум между -30% и -1% от текущей цены
1629                    for (let multiplier = 0.70; multiplier < 0.99; multiplier += 0.01) {
1630                        const testPrice = targetProduct.price * multiplier;
1631                        const testProfit = calculateExpectedProfit(testPrice);
1632                        
1633                        // Выбираем цену с максимальной прибылью (больше текущей)
1634                        if (testProfit > maxProfit) {
1635                            maxProfit = testProfit;
1636                            bestPrice = testPrice;
1637                        }
1638                    }
1639                    
1640                    // Если нашли цену с большей прибылью - используем её
1641                    if (maxProfit > currentProfit) {
1642                        marketCapturePrice = Math.round(bestPrice);
1643                    } else {
1644                        // Если не нашли - используем безопасное снижение на 5%
1645                        marketCapturePrice = Math.round(targetProduct.price * 0.95);
1646                    }
1647                } else {
1648                    // Если нет данных о расходах, снижаем на 5%
1649                    marketCapturePrice = Math.round(targetProduct.price * 0.95);
1650                }
1651                
1652                // Агрессивная: ищем цену для максимизации прибыли с небольшим снижением
1653                // Допускаем падение прибыли до -2%, но стремимся к росту
1654                let aggressivePrice = targetProduct.price;
1655                if (productCosts) {
1656                    const currentProfit = calculateExpectedProfit(targetProduct.price);
1657                    let maxProfit = currentProfit;
1658                    const minAcceptableProfit = currentProfit * 0.98; // Допускаем падение до -2%
1659                    
1660                    // Ищем оптимум между -10% и +5% от текущей цены
1661                    for (let multiplier = 0.90; multiplier <= 1.05; multiplier += 0.01) {
1662                        const testPrice = targetProduct.price * multiplier;
1663                        const testProfit = calculateExpectedProfit(testPrice);
1664                        
1665                        // Выбираем цену с максимальной прибылью, но не ниже минимально допустимой
1666                        if (testProfit >= minAcceptableProfit && testProfit > maxProfit) {
1667                            maxProfit = testProfit;
1668                            aggressivePrice = testPrice;
1669                        }
1670                    }
1671                    aggressivePrice = Math.round(aggressivePrice);
1672                } else {
1673                    // Если нет данных о расходах, умеренное снижение
1674                    aggressivePrice = Math.round(targetProduct.price * 0.95);
1675                }
1676                
1677                // Оптимальная: цена для максимизации прибыли (выше текущей)
1678                // Ищем оптимум между текущей ценой и +30%
1679                let optimalPrice = targetProduct.price;
1680                if (productCosts) {
1681                    let maxProfit = calculateExpectedProfit(targetProduct.price);
1682                    for (let multiplier = 1.05; multiplier <= 1.30; multiplier += 0.01) {
1683                        const testPrice = targetProduct.price * multiplier;
1684                        const testProfit = calculateExpectedProfit(testPrice);
1685                        if (testProfit > maxProfit) {
1686                            maxProfit = testProfit;
1687                            optimalPrice = testPrice;
1688                        }
1689                    }
1690                    optimalPrice = Math.round(optimalPrice);
1691                } else {
1692                    // Если нет данных о расходах, используем рыночную цену
1693                    optimalPrice = Math.round(Math.max(targetProduct.price * 1.10, marketOptimalPrice));
1694                }
1695                
1696                // Рассчитываем базовые цены (цены поручения)
1697                const currentBasePrice = targetProduct.price / (1 - ozonDiscount / 100);
1698                const marketCaptureBasePrice = marketCapturePrice / (1 - ozonDiscount / 100);
1699                const aggressiveBasePrice = aggressivePrice / (1 - ozonDiscount / 100);
1700                const optimalBasePrice = optimalPrice / (1 - ozonDiscount / 100);
1701                
1702                return {
1703                    marketCapture: marketCapturePrice,
1704                    aggressive: aggressivePrice,
1705                    optimal: optimalPrice,
1706                    currentPrice: targetProduct.price,
1707                    currentBasePrice: Math.round(currentBasePrice),
1708                    marketCaptureBasePrice: Math.round(marketCaptureBasePrice),
1709                    aggressiveBasePrice: Math.round(aggressiveBasePrice),
1710                    optimalBasePrice: Math.round(optimalBasePrice),
1711                    currentPosition: targetProduct.position,
1712                    currentProfit: null,
1713                    rpi: rpi.toFixed(1),
1714                    priceChange: {
1715                        marketCapture: Math.round(((marketCapturePrice - targetProduct.price) / targetProduct.price * 100)),
1716                        aggressive: Math.round(((aggressivePrice - targetProduct.price) / targetProduct.price * 100)),
1717                        optimal: Math.round(((optimalPrice - targetProduct.price) / targetProduct.price * 100))
1718                    },
1719                    forecast: {
1720                        marketCapture: calculateForecast(marketCapturePrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts),
1721                        aggressive: calculateForecast(aggressivePrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts),
1722                        optimal: calculateForecast(optimalPrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts)
1723                    }
1724                };
1725            }
1726
1727            // Общие рекомендации
1728            let weightedSum = 0;
1729            let weightSum = 0;
1730            products.forEach(p => {
1731                const positionBonus = p.position <= 5 ? 2 : 1;
1732                const weight = p.revenue * positionBonus;
1733                weightedSum += p.price * weight;
1734                weightSum += weight;
1735            });
1736            const weightedPrice = weightSum > 0 ? weightedSum / weightSum : 0;
1737            
1738            const productsWithConversion = products.map(p => ({
1739                ...p,
1740                conversion: p.orders / p.price
1741            })).sort((a, b) => b.conversion - a.conversion);
1742            const topConversionPrice = productsWithConversion.length > 0 
1743                ? productsWithConversion.slice(0, 3).reduce((sum, p) => sum + p.price, 0) / 3
1744                : 0;
1745            
1746            const top10Prices = products.slice(0, 10).map(p => p.price).sort((a, b) => a - b);
1747            const medianPrice = top10Prices.length > 0 
1748                ? top10Prices[Math.floor(top10Prices.length / 2)]
1749                : 0;
1750
1751            const baseOptimalPrice = (weightedPrice * 0.4 + topConversionPrice * 0.3 + medianPrice * 0.3);
1752
1753            const recommendedPrices = {
1754                marketCapture: {
1755                    price: Math.round(baseOptimalPrice * 0.70),
1756                    strategy: 'Захват рынка',
1757                    description: 'Оптимальная цена ниже текущей для максимизации прибыли'
1758                },
1759                aggressive: {
1760                    price: Math.round(baseOptimalPrice * 0.85),
1761                    strategy: 'Агрессивная',
1762                    description: 'Низкая цена для максимальных продаж и быстрого роста позиций'
1763                },
1764                optimal: {
1765                    price: Math.round(baseOptimalPrice),
1766                    strategy: 'Оптимальная',
1767                    description: 'Баланс между прибылью и объемом продаж'
1768                }
1769            };
1770
1771            // Раздельные рекомендации для целевых брендов
1772            const glsProducts = products.filter(p => p.brand.includes('GLS Pharmaceuticals'));
1773            const skinphoriaProducts = products.filter(p => p.brand.includes('Skinphoria'));
1774            
1775            const brandRecommendations = {};
1776            
1777            // Рекомендации для GLS Pharmaceuticals
1778            if (glsProducts.length > 0) {
1779                const glsAvgPosition = glsProducts.reduce((sum, p) => sum + p.position, 0) / glsProducts.length;
1780                const glsAvgPrice = glsProducts.reduce((sum, p) => sum + p.price, 0) / glsProducts.length;
1781                
1782                brandRecommendations.gls = {
1783                    brand: 'GLS Pharmaceuticals',
1784                    currentProducts: glsProducts.map(p => {
1785                        const priceRec = calculatePriceForProduct(p, products);
1786                        return {
1787                            sku: p.sku,
1788                            name: p.name,
1789                            position: p.position,
1790                            price: p.price,
1791                            revenue: p.revenue,
1792                            orders: p.orders,
1793                            recommendations: priceRec
1794                        };
1795                    }),
1796                    avgPosition: Math.round(glsAvgPosition),
1797                    avgPrice: Math.round(glsAvgPrice)
1798                };
1799            }
1800            
1801            // Рекомендации для Skinphoria
1802            if (skinphoriaProducts.length > 0) {
1803                const skinphoriaAvgPosition = skinphoriaProducts.reduce((sum, p) => sum + p.position, 0) / skinphoriaProducts.length;
1804                const skinphoriaAvgPrice = skinphoriaProducts.reduce((sum, p) => sum + p.price, 0) / skinphoriaProducts.length;
1805                
1806                brandRecommendations.skinphoria = {
1807                    brand: 'Skinphoria',
1808                    currentProducts: skinphoriaProducts.map(p => {
1809                        const priceRec = calculatePriceForProduct(p, products);
1810                        return {
1811                            sku: p.sku,
1812                            name: p.name,
1813                            position: p.position,
1814                            price: p.price,
1815                            revenue: p.revenue,
1816                            orders: p.orders,
1817                            recommendations: priceRec
1818                        };
1819                    }),
1820                    avgPosition: Math.round(skinphoriaAvgPosition),
1821                    avgPrice: Math.round(skinphoriaAvgPrice)
1822                };
1823            }
1824
1825            console.log('Ozon Product Parser: Analysis completed successfully');
1826            
1827            return {
1828                totalRevenue,
1829                totalOrders,
1830                avgPrice: Math.round(totalRevenue / totalOrders),
1831                topProducts,
1832                priceSegments: segments,
1833                elasticity: elasticity !== null ? {
1834                    value: elasticity.toFixed(2),
1835                    interpretation: elasticityInterpretation
1836                } : null,
1837                recommendedPrices,
1838                brandRecommendations
1839            };
1840        } catch (error) {
1841            console.error('Ozon Product Parser: Error in analyzeProducts:', error);
1842            return null;
1843        }
1844    }
1845
1846    // Показываем результаты
1847    async function showResultsModal() {
1848        console.log('Ozon Product Parser: Opening results modal');
1849        
1850        try {
1851            // Получаем результаты по спискам
1852            const allListResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
1853            const allListResults = JSON.parse(allListResultsJson);
1854            const listNames = Object.keys(allListResults);
1855
1856            if (listNames.length === 0) {
1857                alert('Нет сохраненных результатов. Сначала выполните парсинг.');
1858                return;
1859            }
1860
1861            const modal = document.createElement('div');
1862            modal.className = 'ozon-parser-modal';
1863
1864            const content = document.createElement('div');
1865            content.className = 'ozon-parser-modal-content';
1866            content.style.maxWidth = '95vw';
1867            content.style.width = '95vw';
1868
1869            content.innerHTML = `
1870                <div class="ozon-parser-modal-header">Результаты парсинга</div>
1871                <div class="ozon-parser-modal-body">
1872                    <div style="margin-bottom: 20px;">
1873                        <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Выберите список:</label>
1874                        <div style="display: flex; gap: 10px; align-items: center;">
1875                            <select class="ozon-parser-search" id="list-selector" style="flex: 1; margin-bottom: 0;">
1876                                ${listNames.map(listName => {
1877        const list = allListResults[listName];
1878        const date = new Date(list.updatedAt).toLocaleDateString('ru-RU');
1879        const queriesCount = list.queries.length;
1880        return `<option value="${listName}">${listName} (${queriesCount} запросов, обновлен: ${date})</option>`;
1881    }).join('')}
1882                            </select>
1883                            <button class="ozon-parser-btn" id="delete-list-btn" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);">Удалить список</button>
1884                        </div>
1885                    </div>
1886                    <input type="text" class="ozon-parser-search" id="query-search" placeholder="Поиск по запросам">
1887                    <input type="text" class="ozon-parser-search" id="sku-search" placeholder="Поиск по SKU">
1888                    <div class="ozon-parser-tabs" id="query-tabs"></div>
1889                    <div id="results-container"></div>
1890                </div>
1891                <div class="ozon-parser-modal-footer">
1892                    <button class="ozon-parser-btn" id="close-results-btn">Закрыть</button>
1893                </div>
1894            `;
1895
1896            modal.appendChild(content);
1897            document.body.appendChild(modal);
1898
1899            // Текущий выбранный список
1900            let currentListName = listNames[0];
1901            let currentResults = allListResults[currentListName].queries;
1902
1903            // Функция для обновления отображения
1904            async function updateDisplay() {
1905                const queries = Object.keys(currentResults);
1906                queries.sort((a, b) => a.localeCompare(b, 'ru'));
1907                
1908                // Создаем вкладки
1909                createTabs(queries, currentResults, content);
1910                
1911                // Показываем результаты первого запроса
1912                if (queries.length > 0) {
1913                    await displayResults(queries[0], currentResults[queries[0]], content);
1914                }
1915            }
1916
1917            // Обработчик выбора списка
1918            const listSelector = content.querySelector('#list-selector');
1919            listSelector.addEventListener('change', () => {
1920                currentListName = listSelector.value;
1921                currentResults = allListResults[currentListName].queries;
1922                updateDisplay();
1923            });
1924
1925            // Обработчик удаления списка
1926            const deleteListBtn = content.querySelector('#delete-list-btn');
1927            deleteListBtn.addEventListener('click', async () => {
1928                if (!confirm(`Вы уверены, что хотите удалить список "${currentListName}"?`)) {
1929                    return;
1930                }
1931                
1932                // Удаляем список
1933                delete allListResults[currentListName];
1934                await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
1935                
1936                // Также удаляем из сохраненных списков
1937                const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
1938                const savedLists = JSON.parse(savedListsJson);
1939                delete savedLists[currentListName];
1940                await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
1941                
1942                console.log(`Ozon Product Parser: List "${currentListName}" deleted`);
1943                
1944                // Обновляем UI
1945                const remainingLists = Object.keys(allListResults);
1946                if (remainingLists.length === 0) {
1947                    alert('Все списки удалены');
1948                    modal.remove();
1949                    return;
1950                }
1951                
1952                // Переключаемся на первый оставшийся список
1953                currentListName = remainingLists[0];
1954                currentResults = allListResults[currentListName].queries;
1955                
1956                // Обновляем селектор
1957                listSelector.innerHTML = remainingLists.map(listName => {
1958                    const list = allListResults[listName];
1959                    const date = new Date(list.updatedAt).toLocaleDateString('ru-RU');
1960                    const queriesCount = list.queries.length;
1961                    return `<option value="${listName}">${listName} (${queriesCount} запросов, обновлен: ${date})</option>`;
1962                }).join('');
1963                
1964                updateDisplay();
1965            });
1966
1967            // Обработчик закрытия модального окна
1968            const closeBtn = content.querySelector('#close-results-btn');
1969            closeBtn.addEventListener('click', () => {
1970                modal.remove();
1971            });
1972
1973            // Обработчик поиска по запросам
1974            const querySearchInput = content.querySelector('#query-search');
1975            querySearchInput.addEventListener('input', debounce(() => {
1976                const searchValue = querySearchInput.value.trim().toLowerCase();
1977                const queries = Object.keys(currentResults);
1978                queries.sort((a, b) => a.localeCompare(b, 'ru'));
1979                filterQueriesByQuery(searchValue, queries, currentResults, content);
1980            }, 300));
1981
1982            // Обработчик поиска по SKU
1983            const skuSearchInput = content.querySelector('#sku-search');
1984            skuSearchInput.addEventListener('input', debounce(() => {
1985                const searchValue = skuSearchInput.value.trim();
1986                const queries = Object.keys(currentResults);
1987                queries.sort((a, b) => a.localeCompare(b, 'ru'));
1988                filterQueriesBySKU(searchValue, queries, currentResults, content);
1989            }, 300));
1990
1991            // Создаем вкладки для запросов
1992            createTabs(Object.keys(currentResults), currentResults, content);
1993
1994            // Показываем результаты первого запроса
1995            await displayResults(Object.keys(currentResults)[0], currentResults[Object.keys(currentResults)[0]], content);
1996
1997            // Закрытие по клику на фон
1998            modal.addEventListener('click', (e) => {
1999                if (e.target === modal) {
2000                    modal.remove();
2001                }
2002            });
2003
2004            console.log('Ozon Product Parser: Results modal shown');
2005        } catch (error) {
2006            console.error('Ozon Product Parser: Error in showResultsModal:', error);
2007            alert('Ошибка при отображении результатов: ' + error.message);
2008        }
2009    }
2010
2011    // Фильтруем запросы по названию запроса
2012    async function filterQueriesByQuery(queryText, allQueries, results, modalContent) {
2013        if (!queryText) {
2014            createTabs(allQueries, results, modalContent);
2015            await displayResults(allQueries[0], results[allQueries[0]], modalContent);
2016            return;
2017        }
2018
2019        const filteredQueries = allQueries.filter(q => 
2020            q.toLowerCase().includes(queryText)
2021        );
2022
2023        if (filteredQueries.length === 0) {
2024            const container = modalContent.querySelector('#results-container');
2025            container.innerHTML = '<p>Запросы не найдены</p>';
2026            const tabsContainer = modalContent.querySelector('#query-tabs');
2027            tabsContainer.innerHTML = '';
2028            return;
2029        }
2030
2031        createTabs(filteredQueries, results, modalContent);
2032        await displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
2033    }
2034
2035    // Фильтруем запросы по SKU
2036    async function filterQueriesBySKU(sku, allQueries, results, modalContent) {
2037        if (!sku) {
2038            createTabs(allQueries, results, modalContent);
2039            await displayResults(allQueries[0], results[allQueries[0]], modalContent);
2040            return;
2041        }
2042
2043        const filteredQueries = allQueries.filter(query => {
2044            const products = results[query];
2045            return products.some(product => product.sku.includes(sku));
2046        });
2047
2048        if (filteredQueries.length === 0) {
2049            const container = modalContent.querySelector('#results-container');
2050            container.innerHTML = '<p>Товары с таким SKU не найдены ни в одном запросе</p>';
2051            const tabsContainer = modalContent.querySelector('#query-tabs');
2052            tabsContainer.innerHTML = '';
2053            return;
2054        }
2055
2056        createTabs(filteredQueries, results, modalContent);
2057        await displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
2058    }
2059
2060    // Создаем вкладки для запросов
2061    function createTabs(queries, results, modalContent) {
2062        const tabsContainer = modalContent.querySelector('#query-tabs');
2063        tabsContainer.innerHTML = '';
2064        
2065        queries.forEach((query, index) => {
2066            const tab = document.createElement('button');
2067            tab.className = 'ozon-parser-tab' + (index === 0 ? ' active' : '');
2068            tab.textContent = query;
2069            tab.addEventListener('click', async () => {
2070                tabsContainer.querySelectorAll('.ozon-parser-tab').forEach(t => t.classList.remove('active'));
2071                tab.classList.add('active');
2072                await displayResults(query, results[query], modalContent);
2073            });
2074            tabsContainer.appendChild(tab);
2075        });
2076    }
2077
2078    // Отображаем результаты для конкретного запроса
2079    async function displayResults(query, productsData, modalContent) {
2080        console.log('Ozon Product Parser: Displaying results for query:', query);
2081        
2082        const container = modalContent.querySelector('#results-container');
2083        
2084        try {
2085            let products;
2086            
2087            if (Array.isArray(productsData)) {
2088                products = productsData;
2089            } else {
2090                container.innerHTML = '<p>Запросы не найдены</p>';
2091                return;
2092            }
2093            
2094            if (!products || products.length === 0) {
2095                container.innerHTML = '<p>Нет данных для этого запроса</p>';
2096                return;
2097            }
2098            
2099            // Получаем аналитику
2100            const analytics = await analyzeProducts(products);
2101            
2102            if (!analytics) {
2103                container.innerHTML = '<p>Ошибка при анализе данных</p>';
2104                return;
2105            }
2106            
2107            // Получаем скидку Ozon для расчета базовой цены
2108            const ozonDiscount = await GM.getValue('ozon_parser_discount', 50);
2109            
2110            let tableHTML = `
2111                <table class="ozon-parser-results-table">
2112                    <thead>
2113                        <tr>
2114                            <th>Позиция</th>
2115                            <th>SKU</th>
2116                            <th>Название</th>
2117                            <th>Бренд</th>
2118                            <th>Цена</th>
2119                            <th>Выручка</th>
2120                            <th>Заказы</th>
2121                        </tr>
2122                    </thead>
2123                    <tbody>
2124            `;
2125            
2126            products.forEach(product => {
2127                const rowClass = product.isTargetBrand ? ' class="ozon-parser-highlight"' : '';
2128                const skuLink = `https://www.ozon.ru/product/${product.sku}`;
2129                const basePrice = product.price / (1 - ozonDiscount / 100);
2130                
2131                tableHTML += `
2132                    <tr${rowClass}>
2133                        <td>${product.position}</td>
2134                        <td><a href="${skuLink}" target="_blank" class="ozon-parser-sku-link">${product.sku}</a></td>
2135                        <td>${product.name || '—'}</td>
2136                        <td>${product.brand}</td>
2137                        <td>
2138                            <div style="font-weight: 600;">${product.price.toLocaleString('ru-RU')} ₽</div>
2139                            <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${Math.round(basePrice).toLocaleString('ru-RU')} ₽</div>
2140                        </td>
2141                        <td>${product.revenue.toLocaleString('ru-RU')} ₽</td>
2142                        <td>${product.orders.toLocaleString('ru-RU')}</td>
2143                    </tr>
2144                `;
2145            });
2146            
2147            tableHTML += `
2148                            </tbody>
2149                        </table>
2150                    `;
2151            
2152            // Добавляем аналитику
2153            tableHTML += `
2154                <div class="ozon-parser-analytics">
2155                    <div class="ozon-parser-analytics-header">📊 Аналитика запроса</div>
2156                    
2157                    ${analytics.elasticity ? `
2158                    <div class="ozon-parser-analytics-section">
2159                        <div class="ozon-parser-analytics-card">
2160                            <div class="ozon-parser-analytics-card-title">Эластичность запроса</div>
2161                            <div class="ozon-parser-analytics-card-value">${analytics.elasticity.value}</div>
2162                            <div style="font-size: 12px; color: #666; margin-top: 8px;">
2163                                ${analytics.elasticity.interpretation}
2164                            </div>
2165                        </div>
2166                    </div>
2167                    ` : ''}
2168                    
2169                    <div class="ozon-parser-analytics-section">
2170                        <div class="ozon-parser-analytics-section-title">Рекомендованные цены</div>
2171                        <div class="ozon-parser-analytics-grid">
2172                            <div class="ozon-parser-analytics-card" style="border: 2px solid #6c757d;">
2173                                <div class="ozon-parser-analytics-card-title">⚡ Захват рынка</div>
2174                                <div class="ozon-parser-analytics-card-value" style="color: #6c757d;">${analytics.recommendedPrices.marketCapture.price.toLocaleString('ru-RU')} ₽</div>
2175                                <div style="font-size: 12px; color: #666; margin-top: 8px;">Оптимальная цена ниже текущей для максимизации прибыли</div>
2176                            </div>
2177                            <div class="ozon-parser-analytics-card" style="border: 2px solid #dc3545;">
2178                                <div class="ozon-parser-analytics-card-title">✅ Оптимальная</div>
2179                                <div class="ozon-parser-analytics-card-value" style="color: #dc3545;">${analytics.recommendedPrices.aggressive.price.toLocaleString('ru-RU')} ₽</div>
2180                                <div style="font-size: 12px; color: #666; margin-top: 8px;">Низкая цена для максимальных продаж и быстрого роста позиций</div>
2181                            </div>
2182                            <div class="ozon-parser-analytics-card" style="border: 2px solid #28a745;">
2183                                <div class="ozon-parser-analytics-card-title">🔥 Агрессивная</div>
2184                                <div class="ozon-parser-analytics-card-value" style="color: #28a745;">${analytics.recommendedPrices.optimal.price.toLocaleString('ru-RU')} ₽</div>
2185                                <div style="font-size: 12px; color: #666; margin-top: 8px;">Баланс между прибылью и объемом продаж</div>
2186                            </div>
2187                        </div>
2188                        <div class="ozon-parser-info">
2189                            💡 Расчет учитывает: средневзвешенную цену по выручке (вес 40%), оптимальную конверсию цена/заказы (вес 30%), медианную цену топ-10 (вес 30%). Товары из топ-5 позиций имеют удвоенный вес.
2190                        </div>
2191                    </div>
2192                    
2193                    ${analytics.brandRecommendations && Object.keys(analytics.brandRecommendations).length > 0 ? `
2194                    <div class="ozon-parser-analytics-section">
2195                        <div class="ozon-parser-analytics-section-title">🎯 Рекомендации для ваших брендов</div>
2196                        ${analytics.brandRecommendations.gls ? `
2197                        <div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 2px solid #ffc107;">
2198                            <div style="font-size: 18px; font-weight: 600; margin-bottom: 15px; color: #333;">
2199                                ${analytics.brandRecommendations.gls.brand}
2200                            </div>
2201                            <div style="margin-bottom: 15px;">
2202                                <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Текущие товары в выдаче: ${analytics.brandRecommendations.gls.currentProducts.length}</div>
2203                                <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Средняя позиция: ${analytics.brandRecommendations.gls.avgPosition}</div>
2204                                <div style="font-size: 13px; color: #666;">Средняя цена: ${analytics.brandRecommendations.gls.avgPrice.toLocaleString('ru-RU')}</div>
2205                            </div>
2206                            <details style="margin-top: 10px;" open>
2207                                <summary style="cursor: pointer; font-size: 13px; color: #005bff; font-weight: 600;">Показать товары</summary>
2208                                <table class="ozon-parser-analytics-table" style="margin-top: 10px;">
2209                                    <thead>
2210                                        <tr>
2211                                            <th>SKU</th>
2212                                            <th>Название</th>
2213                                            <th>Позиция</th>
2214                                            <th>Текущая цена</th>
2215                                            <th>⚡ Захват рынка</th>
2216                                            <th>✅ Оптимальная</th>
2217                                            <th>🔥 Агрессивная</th>
2218                                        </tr>
2219                                    </thead>
2220                                    <tbody>
2221                                        ${analytics.brandRecommendations.gls.currentProducts.map(p => {
2222        const rec = p.recommendations;
2223        if (!rec) return `
2224                                            <tr>
2225                                                <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
2226                                                <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
2227                                                <td>${p.position}</td>
2228                                                <td>${p.price.toLocaleString('ru-RU')} ₽</td>
2229                                                <td>—</td>
2230                                                <td>—</td>
2231                                                <td>—</td>
2232                                            </tr>
2233                                            `;
2234        return `
2235                                            <tr>
2236                                                <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
2237                                                <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
2238                                                <td>${p.position}</td>
2239                                                <td>
2240                                                    <div style="font-weight: 600;">${p.price.toLocaleString('ru-RU')}</div>
2241                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.currentBasePrice.toLocaleString('ru-RU')}</div>
2242                                                    <div style="font-size: 11px; color: #666; margin-top: 4px;">Выручка: ${p.revenue.toLocaleString('ru-RU')}</div>
2243                                                    <div style="font-size: 11px; color: #666;">Заказы: ${p.orders.toLocaleString('ru-RU')}</div>
2244                                                    ${rec.skuDiscount ? `
2245                                                    <div style="font-size: 11px; color: #ff6b00; font-weight: 600; margin-top: 4px;">
2246                                                        Скидка Ozon: ${rec.skuDiscount}%
2247                                                    </div>
2248                                                    ` : ''}
2249                                                    ${rec.currentProfit !== null ? `
2250                                                    <div style="font-size: 11px; color: #005bff; font-weight: 600; margin-top: 4px;">
2251                                                        Прибыль: ${rec.currentProfit.toFixed(0)}2252                                                    </div>
2253                                                    ` : ''}
2254                                                </td>
2255                                                <td style="background: #e9ecef;">
2256                                                    <div style="font-weight: 600; color: #6c757d;">${rec.marketCapture.toLocaleString('ru-RU')}</div>
2257                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.marketCaptureBasePrice.toLocaleString('ru-RU')}</div>
2258                                                    <div style="font-size: 11px; color: ${rec.priceChange.marketCapture < 0 ? '#dc3545' : '#28a745'};">
2259                                                        ${rec.priceChange.marketCapture > 0 ? '+' : ''}${rec.priceChange.marketCapture}%
2260                                                    </div>
2261                                                    ${rec.forecast && rec.forecast.marketCapture ? `
2262                                                    <div style="font-size: 11px; color: ${rec.forecast.marketCapture.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2263                                                        Выручка: ${rec.forecast.marketCapture.revenueChange > 0 ? '+' : ''}${rec.forecast.marketCapture.revenueChange}%
2264                                                    </div>
2265                                                    <div style="font-size: 11px; color: ${rec.forecast.marketCapture.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2266                                                        Заказы: ${rec.forecast.marketCapture.ordersChange > 0 ? '+' : ''}${rec.forecast.marketCapture.ordersChange}%
2267                                                    </div>
2268                                                     ${rec.forecast.marketCapture.profit !== null ? `
2269                                                     <div style="font-size: 11px; color: ${rec.forecast.marketCapture.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2270                                                         Прибыль: ${rec.forecast.marketCapture.profitChange > 0 ? '+' : ''}${rec.forecast.marketCapture.profitChange}%
2271                                                     </div>
2272                                                     <div style="font-size: 10px; color: #666;">
2273                                                         ${rec.forecast.marketCapture.profit.toFixed(0)}2274                                                     </div>
2275                                                     ` : ''}
2276                                                     ` : ''}
2277                                                </td>
2278                                                <td style="background: #d4edda;">
2279                                                    <div style="font-weight: 600; color: #28a745;">${rec.aggressive.toLocaleString('ru-RU')} ₽</div>
2280                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.aggressiveBasePrice.toLocaleString('ru-RU')} ₽</div>
2281                                                    <div style="font-size: 11px; color: ${rec.priceChange.aggressive < 0 ? '#dc3545' : '#28a745'};">
2282                                                        ${rec.priceChange.aggressive > 0 ? '+' : ''}${rec.priceChange.aggressive}%
2283                                                    </div>
2284                                                    ${rec.forecast && rec.forecast.aggressive ? `
2285                                                    <div style="font-size: 11px; color: ${rec.forecast.aggressive.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2286                                                        Выручка: ${rec.forecast.aggressive.revenueChange > 0 ? '+' : ''}${rec.forecast.aggressive.revenueChange}%
2287                                                    </div>
2288                                                    <div style="font-size: 11px; color: ${rec.forecast.aggressive.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2289                                                        Заказы: ${rec.forecast.aggressive.ordersChange > 0 ? '+' : ''}${rec.forecast.aggressive.ordersChange}%
2290                                                    </div>
2291                                                     ${rec.forecast.aggressive.profit !== null ? `
2292                                                     <div style="font-size: 11px; color: ${rec.forecast.aggressive.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2293                                                         Прибыль: ${rec.forecast.aggressive.profitChange > 0 ? '+' : ''}${rec.forecast.aggressive.profitChange}%
2294                                                     </div>
2295                                                     <div style="font-size: 10px; color: #666;">
2296                                                         ${rec.forecast.aggressive.profit.toFixed(0)}2297                                                     </div>
2298                                                     ` : ''}
2299                                                     ` : ''}
2300                                                </td>
2301                                                <td style="background: #fff3cd;">
2302                                                    <div style="font-weight: 600; color: #dc3545;">${rec.optimal.toLocaleString('ru-RU')} ₽</div>
2303                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.optimalBasePrice.toLocaleString('ru-RU')} ₽</div>
2304                                                    <div style="font-size: 11px; color: ${rec.priceChange.optimal < 0 ? '#dc3545' : '#28a745'};">
2305                                                        ${rec.priceChange.optimal > 0 ? '+' : ''}${rec.priceChange.optimal}%
2306                                                    </div>
2307                                                    ${rec.forecast && rec.forecast.optimal ? `
2308                                                    <div style="font-size: 11px; color: ${rec.forecast.optimal.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2309                                                        Выручка: ${rec.forecast.optimal.revenueChange > 0 ? '+' : ''}${rec.forecast.optimal.revenueChange}%
2310                                                    </div>
2311                                                    <div style="font-size: 11px; color: ${rec.forecast.optimal.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2312                                                        Заказы: ${rec.forecast.optimal.ordersChange > 0 ? '+' : ''}${rec.forecast.optimal.ordersChange}%
2313                                                    </div>
2314                                                     ${rec.forecast.optimal.profit !== null ? `
2315                                                     <div style="font-size: 11px; color: ${rec.forecast.optimal.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2316                                                         Прибыль: ${rec.forecast.optimal.profitChange > 0 ? '+' : ''}${rec.forecast.optimal.profitChange}%
2317                                                     </div>
2318                                                     <div style="font-size: 10px; color: #666;">
2319                                                         ${rec.forecast.optimal.profit.toFixed(0)}2320                                                     </div>
2321                                                     ` : ''}
2322                                                     ` : ''}
2323                                                </td>
2324                                            </tr>
2325                                            `;
2326    }).join('')}
2327                                    </tbody>
2328                                </table>
2329                            </details>
2330                        </div>
2331                        ` : ''}
2332                        ${analytics.brandRecommendations.skinphoria ? `
2333                        <div style="background: white; padding: 20px; border-radius: 8px; border: 2px solid #ffc107;">
2334                            <div style="font-size: 18px; font-weight: 600; margin-bottom: 15px; color: #333;">
2335                                ${analytics.brandRecommendations.skinphoria.brand}
2336                            </div>
2337                            <div style="margin-bottom: 15px;">
2338                                <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Текущие товары в выдаче: ${analytics.brandRecommendations.skinphoria.currentProducts.length}</div>
2339                                <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Средняя позиция: ${analytics.brandRecommendations.skinphoria.avgPosition}</div>
2340                                <div style="font-size: 13px; color: #666;">Средняя цена: ${analytics.brandRecommendations.skinphoria.avgPrice.toLocaleString('ru-RU')}</div>
2341                            </div>
2342                            <details style="margin-top: 10px;" open>
2343                                <summary style="cursor: pointer; font-size: 13px; color: #005bff; font-weight: 600;">Показать товары</summary>
2344                                <table class="ozon-parser-analytics-table" style="margin-top: 10px;">
2345                                    <thead>
2346                                        <tr>
2347                                            <th>SKU</th>
2348                                            <th>Название</th>
2349                                            <th>Позиция</th>
2350                                            <th>Текущая цена</th>
2351                                            <th>⚡ Захват рынка</th>
2352                                            <th>✅ Оптимальная</th>
2353                                            <th>🔥 Агрессивная</th>
2354                                        </tr>
2355                                    </thead>
2356                                    <tbody>
2357                                        ${analytics.brandRecommendations.skinphoria.currentProducts.map(p => {
2358        const rec = p.recommendations;
2359        if (!rec) return `
2360                                            <tr>
2361                                                <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
2362                                                <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
2363                                                <td>${p.position}</td>
2364                                                <td>${p.price.toLocaleString('ru-RU')} ₽</td>
2365                                                <td>—</td>
2366                                                <td>—</td>
2367                                                <td>—</td>
2368                                            </tr>
2369                                            `;
2370        return `
2371                                            <tr>
2372                                                <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
2373                                                <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
2374                                                <td>${p.position}</td>
2375                                                <td>
2376                                                    <div style="font-weight: 600;">${p.price.toLocaleString('ru-RU')}</div>
2377                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.currentBasePrice.toLocaleString('ru-RU')}</div>
2378                                                    <div style="font-size: 11px; color: #666; margin-top: 4px;">Выручка: ${p.revenue.toLocaleString('ru-RU')}</div>
2379                                                    <div style="font-size: 11px; color: #666;">Заказы: ${p.orders.toLocaleString('ru-RU')}</div>
2380                                                    ${rec.skuDiscount ? `
2381                                                    <div style="font-size: 11px; color: #ff6b00; font-weight: 600; margin-top: 4px;">
2382                                                        Скидка Ozon: ${rec.skuDiscount}%
2383                                                    </div>
2384                                                    ` : ''}
2385                                                    ${rec.currentProfit !== null ? `
2386                                                    <div style="font-size: 11px; color: #005bff; font-weight: 600; margin-top: 4px;">
2387                                                        Прибыль: ${rec.currentProfit.toFixed(0)}2388                                                    </div>
2389                                                    ` : ''}
2390                                                </td>
2391                                                <td style="background: #e9ecef;">
2392                                                    <div style="font-weight: 600; color: #6c757d;">${rec.marketCapture.toLocaleString('ru-RU')}</div>
2393                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.marketCaptureBasePrice.toLocaleString('ru-RU')}</div>
2394                                                    <div style="font-size: 11px; color: ${rec.priceChange.marketCapture < 0 ? '#dc3545' : '#28a745'};">
2395                                                        ${rec.priceChange.marketCapture > 0 ? '+' : ''}${rec.priceChange.marketCapture}%
2396                                                    </div>
2397                                                    ${rec.forecast && rec.forecast.marketCapture ? `
2398                                                    <div style="font-size: 11px; color: ${rec.forecast.marketCapture.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2399                                                        Выручка: ${rec.forecast.marketCapture.revenueChange > 0 ? '+' : ''}${rec.forecast.marketCapture.revenueChange}%
2400                                                    </div>
2401                                                    <div style="font-size: 11px; color: ${rec.forecast.marketCapture.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2402                                                        Заказы: ${rec.forecast.marketCapture.ordersChange > 0 ? '+' : ''}${rec.forecast.marketCapture.ordersChange}%
2403                                                    </div>
2404                                                     ${rec.forecast.marketCapture.profit !== null ? `
2405                                                     <div style="font-size: 11px; color: ${rec.forecast.marketCapture.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2406                                                         Прибыль: ${rec.forecast.marketCapture.profitChange > 0 ? '+' : ''}${rec.forecast.marketCapture.profitChange}%
2407                                                     </div>
2408                                                     <div style="font-size: 10px; color: #666;">
2409                                                         ${rec.forecast.marketCapture.profit.toFixed(0)}2410                                                     </div>
2411                                                     ` : ''}
2412                                                     ` : ''}
2413                                                </td>
2414                                                <td style="background: #d4edda;">
2415                                                    <div style="font-weight: 600; color: #28a745;">${rec.aggressive.toLocaleString('ru-RU')} ₽</div>
2416                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.aggressiveBasePrice.toLocaleString('ru-RU')} ₽</div>
2417                                                    <div style="font-size: 11px; color: ${rec.priceChange.aggressive < 0 ? '#dc3545' : '#28a745'};">
2418                                                        ${rec.priceChange.aggressive > 0 ? '+' : ''}${rec.priceChange.aggressive}%
2419                                                    </div>
2420                                                    ${rec.forecast && rec.forecast.aggressive ? `
2421                                                    <div style="font-size: 11px; color: ${rec.forecast.aggressive.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2422                                                        Выручка: ${rec.forecast.aggressive.revenueChange > 0 ? '+' : ''}${rec.forecast.aggressive.revenueChange}%
2423                                                    </div>
2424                                                    <div style="font-size: 11px; color: ${rec.forecast.aggressive.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2425                                                        Заказы: ${rec.forecast.aggressive.ordersChange > 0 ? '+' : ''}${rec.forecast.aggressive.ordersChange}%
2426                                                    </div>
2427                                                     ${rec.forecast.aggressive.profit !== null ? `
2428                                                     <div style="font-size: 11px; color: ${rec.forecast.aggressive.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2429                                                         Прибыль: ${rec.forecast.aggressive.profitChange > 0 ? '+' : ''}${rec.forecast.aggressive.profitChange}%
2430                                                     </div>
2431                                                     <div style="font-size: 10px; color: #666;">
2432                                                         ${rec.forecast.aggressive.profit.toFixed(0)}2433                                                     </div>
2434                                                     ` : ''}
2435                                                     ` : ''}
2436                                                </td>
2437                                                <td style="background: #fff3cd;">
2438                                                    <div style="font-weight: 600; color: #dc3545;">${rec.optimal.toLocaleString('ru-RU')} ₽</div>
2439                                                    <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.optimalBasePrice.toLocaleString('ru-RU')} ₽</div>
2440                                                    <div style="font-size: 11px; color: ${rec.priceChange.optimal < 0 ? '#dc3545' : '#28a745'};">
2441                                                        ${rec.priceChange.optimal > 0 ? '+' : ''}${rec.priceChange.optimal}%
2442                                                    </div>
2443                                                    ${rec.forecast && rec.forecast.optimal ? `
2444                                                    <div style="font-size: 11px; color: ${rec.forecast.optimal.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
2445                                                        Выручка: ${rec.forecast.optimal.revenueChange > 0 ? '+' : ''}${rec.forecast.optimal.revenueChange}%
2446                                                    </div>
2447                                                    <div style="font-size: 11px; color: ${rec.forecast.optimal.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
2448                                                        Заказы: ${rec.forecast.optimal.ordersChange > 0 ? '+' : ''}${rec.forecast.optimal.ordersChange}%
2449                                                    </div>
2450                                                     ${rec.forecast.optimal.profit !== null ? `
2451                                                     <div style="font-size: 11px; color: ${rec.forecast.optimal.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
2452                                                         Прибыль: ${rec.forecast.optimal.profitChange > 0 ? '+' : ''}${rec.forecast.optimal.profitChange}%
2453                                                     </div>
2454                                                     <div style="font-size: 10px; color: #666;">
2455                                                         ${rec.forecast.optimal.profit.toFixed(0)}2456                                                     </div>
2457                                                     ` : ''}
2458                                                     ` : ''}
2459                                                </td>
2460                                            </tr>
2461                                            `;
2462    }).join('')}
2463                                    </tbody>
2464                                </table>
2465                            </details>
2466                        </div>
2467                        ` : ''}
2468                    </div>
2469                    ` : ''}
2470                    
2471                    <div class="ozon-parser-analytics-section">
2472                        <div class="ozon-parser-analytics-section-title">Общая статистика</div>
2473                        <div class="ozon-parser-analytics-grid">
2474                            <div class="ozon-parser-analytics-card">
2475                                <div class="ozon-parser-analytics-card-title">Общая выручка</div>
2476                                <div class="ozon-parser-analytics-card-value">${analytics.totalRevenue.toLocaleString('ru-RU')}</div>
2477                            </div>
2478                            <div class="ozon-parser-analytics-card">
2479                                <div class="ozon-parser-analytics-card-title">Всего заказов</div>
2480                                <div class="ozon-parser-analytics-card-value">${analytics.totalOrders.toLocaleString('ru-RU')}</div>
2481                            </div>
2482                            <div class="ozon-parser-analytics-card">
2483                                <div class="ozon-parser-analytics-card-title">Средний чек</div>
2484                                <div class="ozon-parser-analytics-card-value">${analytics.avgPrice.toLocaleString('ru-RU')}</div>
2485                            </div>
2486                        </div>
2487                    </div>
2488                    
2489                    <div class="ozon-parser-analytics-section">
2490                        <div class="ozon-parser-analytics-section-title">Топ-5 товаров по выручке</div>
2491                        <table class="ozon-parser-analytics-table">
2492                            <thead>
2493                                <tr>
2494                                    <th>Товар</th>
2495                                    <th>Бренд</th>
2496                                    <th>Цена</th>
2497                                    <th>Выручка</th>
2498                                    <th>Доля</th>
2499                                </tr>
2500                            </thead>
2501                            <tbody>
2502            `;
2503            
2504            analytics.topProducts.forEach(product => {
2505                tableHTML += `
2506                    <tr>
2507                        <td style="max-width: 300px; white-space: normal;">${product.name || '—'}</td>
2508                        <td>${product.brand}</td>
2509                        <td>${product.price.toLocaleString('ru-RU')}</td>
2510                        <td>${product.revenue.toLocaleString('ru-RU')}</td>
2511                        <td>${product.revenueShare}%</td>
2512                    </tr>
2513                `;
2514            });
2515            
2516            tableHTML += `
2517                            </tbody>
2518                        </table>
2519                    </div>
2520                    
2521                    <div class="ozon-parser-analytics-section">
2522                        <div class="ozon-parser-analytics-section-title">Ценовые сегменты</div>
2523                        <table class="ozon-parser-analytics-table">
2524                            <thead>
2525                                <tr>
2526                                    <th>Сегмент</th>
2527                                    <th>Диапазон цен</th>
2528                                    <th>Товаров</th>
2529                                    <th>Средняя цена</th>
2530                                    <th>Выручка</th>
2531                                    <th>Доля выручки</th>
2532                                </tr>
2533                            </thead>
2534                            <tbody>
2535            `;
2536            
2537            analytics.priceSegments.forEach(segment => {
2538                tableHTML += `
2539                    <tr>
2540                        <td>${segment.name}</td>
2541                        <td>${segment.priceRange}</td>
2542                        <td>${segment.count}</td>
2543                        <td>${segment.avgPrice.toLocaleString('ru-RU')}</td>
2544                        <td>${segment.revenue.toLocaleString('ru-RU')}</td>
2545                        <td>
2546                            <div style="display: flex; align-items: center; gap: 10px;">
2547                                <div class="ozon-parser-analytics-bar" style="width: ${segment.revenueShare}%; min-width: 20px;"></div>
2548                                <span>${segment.revenueShare}%</span>
2549                            </div>
2550                        </td>
2551                    </tr>
2552                `;
2553            });
2554            
2555            tableHTML += `
2556                            </tbody>
2557                        </table>
2558                    </div>
2559                </div>
2560            `;
2561            
2562            container.innerHTML = tableHTML;
2563            console.log('Ozon Product Parser: Results displayed successfully');
2564        } catch (error) {
2565            console.error('Ozon Product Parser: Error in displayResults:', error);
2566            container.innerHTML = '<p>Ошибка при отображении результатов: ' + error.message + '</p>';
2567        }
2568    }
2569
2570    // Инициализация
2571    function init() {
2572        console.log('Ozon Product Parser: Initializing...');
2573        
2574        if (document.readyState === 'loading') {
2575            document.addEventListener('DOMContentLoaded', () => {
2576                addStyles();
2577                createUI();
2578                continueParsingIfActive();
2579                continueDiscountCalculationIfActive();
2580            });
2581        } else {
2582            addStyles();
2583            createUI();
2584            continueParsingIfActive();
2585            continueDiscountCalculationIfActive();
2586        }
2587    }
2588
2589    init();
2590})();
Ozon Product Parser | Robomonkey