Size
244.5 KB
Version
1.8.58
Created
Mar 30, 2026
Updated
17 days ago
1// ==UserScript==
2// @name Ozon Product Parser
3// @description Парсер товаров с Ozon для анализа поисковой выдачи
4// @version 1.8.58
5// @match https://ozon.ru/*
6// @match https://www.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 cursor: move;
40 user-select: none;
41 }
42 .ozon-parser-container.collapsed {
43 gap: 0;
44 }
45 .ozon-parser-toggle-btn {
46 background: linear-gradient(135deg, #005bff 0%, #0043c7 100%);
47 color: white;
48 border: none;
49 padding: 12px;
50 border-radius: 8px;
51 cursor: pointer;
52 font-size: 18px;
53 font-weight: 600;
54 box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3);
55 transition: all 0.3s ease;
56 width: 44px;
57 height: 44px;
58 display: flex;
59 align-items: center;
60 justify-content: center;
61 }
62 .ozon-parser-toggle-btn:hover {
63 background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%);
64 box-shadow: 0 6px 16px rgba(0, 91, 255, 0.4);
65 transform: translateY(-2px);
66 }
67 .ozon-parser-buttons-wrapper {
68 display: flex;
69 gap: 10px;
70 transition: all 0.3s ease;
71 }
72 .ozon-parser-buttons-wrapper.hidden {
73 display: none;
74 }
75 .ozon-parser-btn {
76 background: linear-gradient(135deg, #005bff 0%, #0043c7 100%);
77 color: white;
78 border: none;
79 padding: 12px 24px;
80 border-radius: 8px;
81 cursor: pointer;
82 font-size: 14px;
83 font-weight: 600;
84 box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3);
85 transition: all 0.3s ease;
86 }
87 .ozon-parser-btn:hover {
88 background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%);
89 box-shadow: 0 6px 16px rgba(0, 91, 255, 0.4);
90 transform: translateY(-2px);
91 }
92 .ozon-parser-btn:active {
93 transform: translateY(0);
94 }
95 .ozon-parser-btn.secondary {
96 background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
97 box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
98 }
99 .ozon-parser-btn.secondary:hover {
100 background: linear-gradient(135deg, #1e7e34 0%, #155724 100%);
101 box-shadow: 0 6px 16px rgba(40, 167, 69, 0.4);
102 }
103 .ozon-parser-modal {
104 position: fixed;
105 top: 0;
106 left: 0;
107 width: 100%;
108 height: 100%;
109 background: rgba(0, 0, 0, 0.7);
110 display: flex;
111 justify-content: center;
112 align-items: center;
113 z-index: 10001;
114 }
115 .ozon-parser-modal-content {
116 background: white;
117 padding: 30px;
118 border-radius: 12px;
119 max-width: 800px;
120 width: 90%;
121 max-height: 80vh;
122 overflow-y: auto;
123 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
124 }
125 .ozon-parser-modal-header {
126 font-size: 24px;
127 font-weight: 700;
128 margin-bottom: 20px;
129 color: #333;
130 }
131 .ozon-parser-modal-body {
132 margin-bottom: 20px;
133 }
134 .ozon-parser-textarea {
135 width: 100%;
136 min-height: 200px;
137 padding: 12px;
138 border: 2px solid #e0e0e0;
139 border-radius: 8px;
140 font-size: 14px;
141 font-family: inherit;
142 resize: vertical;
143 box-sizing: border-box;
144 }
145 .ozon-parser-textarea:focus {
146 outline: none;
147 border-color: #005bff;
148 }
149 .ozon-parser-modal-footer {
150 display: flex;
151 gap: 10px;
152 justify-content: flex-end;
153 }
154 .ozon-parser-progress {
155 margin-top: 20px;
156 padding: 15px;
157 background: #f8f9fa;
158 border-radius: 8px;
159 font-size: 14px;
160 color: #333;
161 }
162 .ozon-parser-progress-bar {
163 width: 100%;
164 height: 8px;
165 background: #e0e0e0;
166 border-radius: 4px;
167 margin-top: 10px;
168 overflow: hidden;
169 }
170 .ozon-parser-progress-fill {
171 height: 100%;
172 background: linear-gradient(90deg, #005bff 0%, #0043c7 100%);
173 transition: width 0.3s ease;
174 }
175 .ozon-parser-results-table {
176 width: 100%;
177 border-collapse: collapse;
178 margin-top: 20px;
179 font-size: 13px;
180 }
181 .ozon-parser-results-table th,
182 .ozon-parser-results-table td {
183 padding: 12px;
184 text-align: left;
185 border-bottom: 1px solid #e0e0e0;
186 white-space: nowrap;
187 }
188 .ozon-parser-results-table th {
189 background: #f8f9fa;
190 font-weight: 600;
191 color: #333;
192 position: sticky;
193 top: 0;
194 }
195 .ozon-parser-results-table td:nth-child(3) {
196 max-width: 500px;
197 white-space: normal;
198 word-wrap: break-word;
199 }
200 .ozon-parser-results-table tr:hover {
201 background: #f8f9fa;
202 }
203 .ozon-parser-highlight {
204 background: #fff3cd !important;
205 font-weight: 600;
206 }
207 .ozon-parser-sku-link {
208 color: #005bff;
209 text-decoration: none;
210 font-weight: 600;
211 }
212 .ozon-parser-sku-link:hover {
213 text-decoration: underline;
214 }
215 .ozon-parser-tabs {
216 display: flex;
217 gap: 5px;
218 margin-bottom: 20px;
219 flex-wrap: wrap;
220 }
221 .ozon-parser-tab {
222 padding: 10px 20px;
223 background: #f8f9fa;
224 border: none;
225 border-radius: 6px;
226 cursor: pointer;
227 font-size: 14px;
228 transition: all 0.2s ease;
229 }
230 .ozon-parser-tab:hover {
231 background: #e9ecef;
232 }
233 .ozon-parser-tab.active {
234 background: #005bff;
235 color: white;
236 font-weight: 600;
237 }
238 .ozon-parser-info {
239 padding: 10px;
240 background: #e7f3ff;
241 border-left: 4px solid #005bff;
242 border-radius: 4px;
243 margin-bottom: 15px;
244 font-size: 13px;
245 color: #333;
246 }
247 .ozon-parser-search {
248 width: 100%;
249 padding: 10px 12px;
250 border: 2px solid #e0e0e0;
251 border-radius: 8px;
252 font-size: 14px;
253 margin-bottom: 15px;
254 box-sizing: border-box;
255 }
256 .ozon-parser-search:focus {
257 outline: none;
258 border-color: #005bff;
259 }
260 .ozon-parser-search::placeholder {
261 color: #999;
262 }
263 .ozon-parser-analytics {
264 margin-top: 30px;
265 padding: 20px;
266 background: #f8f9fa;
267 border-radius: 8px;
268 }
269 .ozon-parser-analytics-header {
270 font-size: 20px;
271 font-weight: 700;
272 margin-bottom: 20px;
273 color: #333;
274 }
275 .ozon-parser-analytics-section {
276 margin-bottom: 25px;
277 }
278 .ozon-parser-analytics-section-title {
279 font-size: 16px;
280 font-weight: 600;
281 margin-bottom: 12px;
282 color: #005bff;
283 }
284 .ozon-parser-analytics-grid {
285 display: grid;
286 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
287 gap: 15px;
288 margin-bottom: 20px;
289 }
290 .ozon-parser-analytics-card {
291 background: white;
292 padding: 15px;
293 border-radius: 8px;
294 border: 1px solid #e0e0e0;
295 }
296 .ozon-parser-analytics-card-title {
297 font-size: 13px;
298 color: #666;
299 margin-bottom: 8px;
300 }
301 .ozon-parser-analytics-card-value {
302 font-size: 24px;
303 font-weight: 700;
304 color: #333;
305 }
306 .ozon-parser-analytics-table {
307 width: 100%;
308 border-collapse: collapse;
309 background: white;
310 border-radius: 8px;
311 overflow: hidden;
312 }
313 .ozon-parser-analytics-table th,
314 .ozon-parser-analytics-table td {
315 padding: 12px;
316 text-align: left;
317 border-bottom: 1px solid #e0e0e0;
318 }
319 .ozon-parser-analytics-table th {
320 background: #f8f9fa;
321 font-weight: 600;
322 color: #333;
323 font-size: 13px;
324 }
325 .ozon-parser-analytics-table td {
326 font-size: 13px;
327 }
328 .ozon-parser-analytics-bar {
329 height: 20px;
330 background: linear-gradient(90deg, #005bff 0%, #0043c7 100%);
331 border-radius: 4px;
332 transition: width 0.3s ease;
333 }
334 .ozon-parser-details-panel {
335 position: fixed;
336 top: 0;
337 right: -600px;
338 width: 600px;
339 height: 100vh;
340 background: white;
341 box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
342 z-index: 10002;
343 overflow-y: auto;
344 transition: right 0.3s ease;
345 }
346 .ozon-parser-details-panel.open {
347 right: 0;
348 }
349 .ozon-parser-details-panel-header {
350 position: sticky;
351 top: 0;
352 background: white;
353 padding: 20px;
354 border-bottom: 2px solid #e0e0e0;
355 display: flex;
356 justify-content: space-between;
357 align-items: center;
358 z-index: 1;
359 }
360 .ozon-parser-details-panel-title {
361 font-size: 18px;
362 font-weight: 700;
363 color: #333;
364 }
365 .ozon-parser-details-panel-close {
366 background: #dc3545;
367 color: white;
368 border: none;
369 padding: 8px 16px;
370 border-radius: 6px;
371 cursor: pointer;
372 font-size: 14px;
373 font-weight: 600;
374 transition: all 0.2s ease;
375 }
376 .ozon-parser-details-panel-close:hover {
377 background: #c82333;
378 }
379 .ozon-parser-details-panel-body {
380 padding: 20px;
381 }
382 .ozon-parser-clickable-row {
383 cursor: pointer;
384 transition: background 0.2s ease;
385 }
386 .ozon-parser-clickable-row:hover {
387 background: #e7f3ff !important;
388 }
389 `;
390 const styleElement = document.createElement('style');
391 styleElement.textContent = styles;
392 document.head.appendChild(styleElement);
393 console.log('Ozon Product Parser: Styles added');
394 }
395
396 // Обновляем счетчик высоких цен
397 async function updateHighPriceCounter() {
398 const highPriceBtn = document.querySelector('#high-price-btn');
399 if (!highPriceBtn) return;
400
401 const highPriceDataJson = await GM.getValue('ozon_parser_high_price', '[]');
402 const highPriceData = JSON.parse(highPriceDataJson);
403
404 highPriceBtn.textContent = `Высокая цена (${highPriceData.length})`;
405 }
406
407 // Показываем модальное окно с ценами
408 async function showPricesModal() {
409 console.log('Ozon Product Parser: Opening prices modal');
410
411 try {
412 // Получаем результаты парсинга
413 const allListResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
414 const allListResults = JSON.parse(allListResultsJson);
415
416 if (Object.keys(allListResults).length === 0) {
417 alert('Нет данных для анализа цен. Сначала выполните парсинг.');
418 return;
419 }
420
421 // Загружаем сохраненные привязки SKU к запросам
422 const skuQueryBindingsJson = await GM.getValue('ozon_parser_sku_query_bindings', '{}');
423 const skuQueryBindings = JSON.parse(skuQueryBindingsJson);
424
425 // Собираем все товары наших брендов из всех запросов
426 const ourProductsData = [];
427
428 for (const listName in allListResults) {
429 const listData = allListResults[listName];
430 for (const query in listData.queries) {
431 const products = listData.queries[query];
432
433 // Находим наши товары
434 const ourProducts = products.filter(p => p.isTargetBrand);
435
436 ourProducts.forEach(ourProduct => {
437 // Проверяем, есть ли привязка для этого SKU
438 const boundQuery = skuQueryBindings[ourProduct.sku];
439
440 // Если есть привязка и это не тот запрос - пропускаем
441 if (boundQuery && boundQuery !== query) {
442 return;
443 }
444
445 // Находим топ-3 конкурентов по позиции в выдаче (а не по выручке)
446 const competitors = products.filter(p => !p.isTargetBrand && p.price > 0);
447
448 // Сортируем конкурентов по позиции в выдаче
449 const sortedCompetitors = [...competitors].sort((a, b) => a.position - b.position);
450
451 // Берем первых 3 конкурентов по позиции
452 const top3Competitors = sortedCompetitors.slice(0, 3);
453
454 if (top3Competitors.length > 0) {
455 // Рассчитываем цены конкурентов пятью способами
456 const competitorPrices = top3Competitors.map(c => c.price);
457 const competitorRevenues = top3Competitors.map(c => c.revenue);
458
459 // 1. Максимум по топ-3
460 const competitorMaxPrice = Math.max(...competitorPrices);
461
462 // 2. Среднее по топ-3
463 const competitorAvgPrice = competitorPrices.reduce((sum, p) => sum + p, 0) / competitorPrices.length;
464
465 // 3. Средневзвешенное по выручке
466 const totalRevenue = competitorRevenues.reduce((sum, r) => sum + r, 0);
467 const competitorWeightedPrice = top3Competitors.reduce((sum, c) => sum + (c.price * c.revenue), 0) / totalRevenue;
468
469 // 4. Минимальная по топ-3
470 const competitorMinPrice = Math.min(...competitorPrices);
471
472 // 5. Медиана по топ-3
473 const sortedPrices = [...competitorPrices].sort((a, b) => a - b);
474 const competitorMedianPrice = sortedPrices[Math.floor(sortedPrices.length / 2)];
475
476 // Рассчитываем среднюю цену (выручка / заказы)
477 const ourAvgPrice = ourProduct.orders > 0 ? ourProduct.revenue / ourProduct.orders : 0;
478 const competitorAvgPrices = top3Competitors.map(c => c.orders > 0 ? c.revenue / c.orders : 0);
479 const competitorAvgPriceValue = competitorAvgPrices.reduce((sum, p) => sum + p, 0) / competitorAvgPrices.length;
480
481 // Рассчитываем выручку конкурентов (среднее)
482 const competitorRevenue = competitorRevenues.reduce((sum, r) => sum + r, 0) / competitorRevenues.length;
483
484 ourProductsData.push({
485 sku: ourProduct.sku,
486 name: ourProduct.name,
487 query: query,
488 listName: listName,
489
490 // Текущая цена
491 ourCurrentPrice: ourProduct.price,
492 competitorMaxPrice: competitorMaxPrice,
493 competitorAvgPrice: competitorAvgPrice,
494 competitorWeightedPrice: competitorWeightedPrice,
495 competitorMinPrice: competitorMinPrice,
496 competitorMedianPrice: competitorMedianPrice,
497
498 // Средняя цена
499 ourAvgPrice: ourAvgPrice,
500 competitorAvgPriceValue: competitorAvgPriceValue,
501
502 // Выручка
503 ourRevenue: ourProduct.revenue,
504 competitorRevenue: competitorRevenue,
505
506 // Для расчета дельт
507 top3Competitors: top3Competitors,
508
509 // Флаг привязки
510 isBound: !!boundQuery
511 });
512 }
513 });
514 }
515 }
516
517 if (ourProductsData.length === 0) {
518 alert('Нет товаров наших брендов в результатах парсинга.');
519 return;
520 }
521
522 const modal = document.createElement('div');
523 modal.className = 'ozon-parser-modal';
524
525 const content = document.createElement('div');
526 content.className = 'ozon-parser-modal-content';
527 content.style.maxWidth = '98vw';
528 content.style.width = '98vw';
529
530 // Состояние сортировки
531 let sortColumn = null;
532 let sortDirection = 'asc';
533
534 // Функция для экспорта в CSV
535 function exportToCSV(data, method, filters) {
536 // Фильтруем данные так же, как в таблице
537 let filteredData = [...data];
538
539 // Применяем фильтр по запросу
540 if (filters.querySearch) {
541 filteredData = filteredData.filter(item =>
542 item.query.toLowerCase().includes(filters.querySearch.toLowerCase())
543 );
544 }
545
546 // Применяем фильтр по SKU
547 if (filters.skuSearch) {
548 filteredData = filteredData.filter(item =>
549 item.sku.includes(filters.skuSearch)
550 );
551 }
552
553 // Применяем фильтры по выручке
554 if (filters.revenue > 0) {
555 filteredData = filteredData.filter(item => {
556 const revenueDelta = ((item.ourRevenue - item.competitorRevenue) / item.competitorRevenue) * 100;
557 if (filters.revenueDirection === 'less') {
558 return revenueDelta <= -filters.revenue;
559 } else {
560 return revenueDelta >= filters.revenue;
561 }
562 });
563 }
564
565 // Применяем фильтры по текущей цене
566 if (filters.currentPrice > 0) {
567 filteredData = filteredData.filter(item => {
568 let competitorPrice;
569 if (method === 'max') competitorPrice = item.competitorMaxPrice;
570 else if (method === 'avg') competitorPrice = item.competitorAvgPrice;
571 else if (method === 'min') competitorPrice = item.competitorMinPrice;
572 else if (method === 'median') competitorPrice = item.competitorMedianPrice;
573 else competitorPrice = item.competitorWeightedPrice;
574
575 const priceDelta = ((item.ourCurrentPrice - competitorPrice) / competitorPrice) * 100;
576 if (filters.currentPriceDirection === 'less') {
577 return priceDelta <= -filters.currentPrice;
578 } else {
579 return priceDelta >= filters.currentPrice;
580 }
581 });
582 }
583
584 // Применяем фильтры по средней цене
585 if (filters.avgPrice > 0) {
586 filteredData = filteredData.filter(item => {
587 const avgPriceDelta = ((item.ourAvgPrice - item.competitorAvgPriceValue) / item.competitorAvgPriceValue) * 100;
588 if (filters.avgPriceDirection === 'less') {
589 return avgPriceDelta <= -filters.avgPrice;
590 } else {
591 return avgPriceDelta >= filters.avgPrice;
592 }
593 });
594 }
595
596 // Формируем CSV
597 const methodNames = {
598 'max': 'Максимум',
599 'avg': 'Среднее',
600 'weighted': 'Средневзвешенное',
601 'min': 'Минимум',
602 'median': 'Медиана'
603 };
604
605 let csv = '\uFEFF'; // BOM для корректного отображения кириллицы в Excel
606 csv += 'SKU;Название;Запрос;Список;';
607 csv += `Наша текущая цена;Конкуренты текущая (${methodNames[method]});Дельта текущей цены (%);`;
608 csv += 'Наша средняя цена;Конкуренты средняя;Дельта средней цены (%);';
609 csv += 'Наша выручка;Конкуренты выручка;Дельта выручки (%)\n';
610
611 filteredData.forEach(item => {
612 // Выбираем цену конкурентов в зависимости от метода
613 let competitorPrice;
614 if (method === 'max') competitorPrice = item.competitorMaxPrice;
615 else if (method === 'avg') competitorPrice = item.competitorAvgPrice;
616 else if (method === 'min') competitorPrice = item.competitorMinPrice;
617 else if (method === 'median') competitorPrice = item.competitorMedianPrice;
618 else competitorPrice = item.competitorWeightedPrice;
619
620 // Рассчитываем дельты
621 const currentPriceDelta = ((item.ourCurrentPrice - competitorPrice) / competitorPrice) * 100;
622 const avgPriceDelta = ((item.ourAvgPrice - item.competitorAvgPriceValue) / item.competitorAvgPriceValue) * 100;
623 const revenueDelta = ((item.ourRevenue - item.competitorRevenue) / item.competitorRevenue) * 100;
624
625 // Экранируем кавычки и переносы строк в названии
626 const safeName = (item.name || '—').replace(/"/g, '""').replace(/\n/g, ' ');
627
628 // Форматируем дельты: добавляем знак = в начале, чтобы Excel интерпретировал как формулу, возвращающую текст
629 const currentPriceDeltaStr = `="${currentPriceDelta.toFixed(1)}"`;
630 const avgPriceDeltaStr = `="${avgPriceDelta.toFixed(1)}"`;
631 const revenueDeltaStr = `="${revenueDelta.toFixed(1)}"`;
632
633 csv += `${item.sku};"${safeName}";${item.query};${item.listName};`;
634 csv += `${Math.round(item.ourCurrentPrice)};${Math.round(competitorPrice)};${currentPriceDeltaStr};`;
635 csv += `${Math.round(item.ourAvgPrice)};${Math.round(item.competitorAvgPriceValue)};${avgPriceDeltaStr};`;
636 csv += `${Math.round(item.ourRevenue)};${Math.round(item.competitorRevenue)};${revenueDeltaStr}\n`;
637 });
638
639 // Создаем и скачиваем файл
640 const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
641 const link = document.createElement('a');
642 const url = URL.createObjectURL(blob);
643 link.setAttribute('href', url);
644 link.setAttribute('download', `ozon_prices_${new Date().toISOString().slice(0, 10)}.csv`);
645 link.style.visibility = 'hidden';
646 document.body.appendChild(link);
647 link.click();
648 document.body.removeChild(link);
649
650 console.log(`Ozon Product Parser: Exported ${filteredData.length} products to CSV`);
651 }
652
653 // Функция для отображения таблицы с учетом выбранного метода и фильтров
654 function displayPricesTable(method, filters) {
655 let filteredData = [...ourProductsData];
656
657 // Применяем фильтр по запросу
658 if (filters.querySearch) {
659 filteredData = filteredData.filter(item =>
660 item.query.toLowerCase().includes(filters.querySearch.toLowerCase())
661 );
662 }
663
664 // Применяем фильтр по SKU
665 if (filters.skuSearch) {
666 filteredData = filteredData.filter(item =>
667 item.sku.includes(filters.skuSearch)
668 );
669 }
670
671 // Применяем фильтры по выручке
672 if (filters.revenue > 0) {
673 filteredData = filteredData.filter(item => {
674 const revenueDelta = ((item.ourRevenue - item.competitorRevenue) / item.competitorRevenue) * 100;
675 if (filters.revenueDirection === 'less') {
676 return revenueDelta <= -filters.revenue;
677 } else {
678 return revenueDelta >= filters.revenue;
679 }
680 });
681 }
682
683 // Применяем фильтры по текущей цене
684 if (filters.currentPrice > 0) {
685 filteredData = filteredData.filter(item => {
686 let competitorPrice;
687 if (method === 'max') competitorPrice = item.competitorMaxPrice;
688 else if (method === 'avg') competitorPrice = item.competitorAvgPrice;
689 else if (method === 'min') competitorPrice = item.competitorMinPrice;
690 else if (method === 'median') competitorPrice = item.competitorMedianPrice;
691 else competitorPrice = item.competitorWeightedPrice;
692
693 const priceDelta = ((item.ourCurrentPrice - competitorPrice) / competitorPrice) * 100;
694 if (filters.currentPriceDirection === 'less') {
695 return priceDelta <= -filters.currentPrice;
696 } else {
697 return priceDelta >= filters.currentPrice;
698 }
699 });
700 }
701
702 // Применяем фильтры по средней цене
703 if (filters.avgPrice > 0) {
704 filteredData = filteredData.filter(item => {
705 const avgPriceDelta = ((item.ourAvgPrice - item.competitorAvgPriceValue) / item.competitorAvgPriceValue) * 100;
706 if (filters.avgPriceDirection === 'less') {
707 return avgPriceDelta <= -filters.avgPrice;
708 } else {
709 return avgPriceDelta >= filters.avgPrice;
710 }
711 });
712 }
713
714 // Применяем сортировку
715 if (sortColumn) {
716 filteredData.sort((a, b) => {
717 let aVal, bVal;
718
719 switch(sortColumn) {
720 case 'sku':
721 aVal = a.sku;
722 bVal = b.sku;
723 break;
724 case 'name':
725 aVal = (a.name || '').toLowerCase();
726 bVal = (b.name || '').toLowerCase();
727 break;
728 case 'query':
729 aVal = a.query.toLowerCase();
730 bVal = b.query.toLowerCase();
731 break;
732 case 'ourCurrentPrice':
733 aVal = a.ourCurrentPrice;
734 bVal = b.ourCurrentPrice;
735 break;
736 case 'competitorPrice':
737 if (method === 'max') {
738 aVal = a.competitorMaxPrice;
739 bVal = b.competitorMaxPrice;
740 } else if (method === 'avg') {
741 aVal = a.competitorAvgPrice;
742 bVal = b.competitorAvgPrice;
743 } else {
744 aVal = a.competitorWeightedPrice;
745 bVal = b.competitorWeightedPrice;
746 }
747 break;
748 case 'currentPriceDelta':
749 let compPriceA = method === 'max' ? a.competitorMaxPrice : (method === 'avg' ? a.competitorAvgPrice : a.competitorWeightedPrice);
750 let compPriceB = method === 'max' ? b.competitorMaxPrice : (method === 'avg' ? b.competitorAvgPrice : b.competitorWeightedPrice);
751 aVal = ((a.ourCurrentPrice - compPriceA) / compPriceA) * 100;
752 bVal = ((b.ourCurrentPrice - compPriceB) / compPriceB) * 100;
753 break;
754 case 'ourAvgPrice':
755 aVal = a.ourAvgPrice;
756 bVal = b.ourAvgPrice;
757 break;
758 case 'competitorAvgPrice':
759 aVal = a.competitorAvgPriceValue;
760 bVal = b.competitorAvgPriceValue;
761 break;
762 case 'avgPriceDelta':
763 aVal = ((a.ourAvgPrice - a.competitorAvgPriceValue) / a.competitorAvgPriceValue) * 100;
764 bVal = ((b.ourAvgPrice - b.competitorAvgPriceValue) / b.competitorAvgPriceValue) * 100;
765 break;
766 case 'ourRevenue':
767 aVal = a.ourRevenue;
768 bVal = b.ourRevenue;
769 break;
770 case 'competitorRevenue':
771 aVal = a.competitorRevenue;
772 bVal = b.competitorRevenue;
773 break;
774 case 'revenueDelta':
775 aVal = ((a.ourRevenue - a.competitorRevenue) / a.competitorRevenue) * 100;
776 bVal = ((b.ourRevenue - b.competitorRevenue) / b.competitorRevenue) * 100;
777 break;
778 default:
779 return 0;
780 }
781
782 if (typeof aVal === 'string') {
783 return sortDirection === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
784 } else {
785 return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
786 }
787 });
788 }
789
790 let tableHTML = `
791 <div class="ozon-parser-info">
792 Найдено товаров: ${filteredData.length} из ${ourProductsData.length}
793 </div>
794 <table class="ozon-parser-results-table">
795 <thead>
796 <tr>
797 <th rowspan="2" data-sort="sku" style="cursor: pointer; user-select: none;">SKU ${sortColumn === 'sku' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
798 <th rowspan="2" data-sort="name" style="cursor: pointer; user-select: none;">Название ${sortColumn === 'name' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
799 <th rowspan="2" data-sort="query" style="cursor: pointer; user-select: none;">Запрос ${sortColumn === 'query' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
800 <th colspan="3">Текущая цена</th>
801 <th colspan="3">Средняя цена</th>
802 <th colspan="3">Выручка</th>
803 </tr>
804 <tr>
805 <th data-sort="ourCurrentPrice" style="cursor: pointer; user-select: none;">Наша ${sortColumn === 'ourCurrentPrice' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
806 <th data-sort="competitorPrice" style="cursor: pointer; user-select: none;">Конкуренты ${sortColumn === 'competitorPrice' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
807 <th data-sort="currentPriceDelta" style="cursor: pointer; user-select: none;">Дельта ${sortColumn === 'currentPriceDelta' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
808 <th data-sort="ourAvgPrice" style="cursor: pointer; user-select: none;">Наша ${sortColumn === 'ourAvgPrice' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
809 <th data-sort="competitorAvgPrice" style="cursor: pointer; user-select: none;">Конкуренты ${sortColumn === 'competitorAvgPrice' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
810 <th data-sort="avgPriceDelta" style="cursor: pointer; user-select: none;">Дельта ${sortColumn === 'avgPriceDelta' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
811 <th data-sort="ourRevenue" style="cursor: pointer; user-select: none;">Наша ${sortColumn === 'ourRevenue' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
812 <th data-sort="competitorRevenue" style="cursor: pointer; user-select: none;">Конкуренты ${sortColumn === 'competitorRevenue' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
813 <th data-sort="revenueDelta" style="cursor: pointer; user-select: none;">Дельта ${sortColumn === 'revenueDelta' ? (sortDirection === 'asc' ? '▲' : '▼') : ''}</th>
814 </tr>
815 </thead>
816 <tbody>
817 `;
818
819 filteredData.forEach(item => {
820 // Выбираем цену конкурентов в зависимости от метода
821 let competitorPrice;
822 if (method === 'max') competitorPrice = item.competitorMaxPrice;
823 else if (method === 'avg') competitorPrice = item.competitorAvgPrice;
824 else if (method === 'min') competitorPrice = item.competitorMinPrice;
825 else if (method === 'median') competitorPrice = item.competitorMedianPrice;
826 else competitorPrice = item.competitorWeightedPrice;
827
828 // Рассчитываем дельты
829 const currentPriceDelta = ((item.ourCurrentPrice - competitorPrice) / competitorPrice) * 100;
830 const avgPriceDelta = ((item.ourAvgPrice - item.competitorAvgPriceValue) / item.competitorAvgPriceValue) * 100;
831 const revenueDelta = ((item.ourRevenue - item.competitorRevenue) / item.competitorRevenue) * 100;
832
833 // Добавляем индикатор привязки к запросу
834 const boundIndicator = item.isBound ? '<span style="color: #28a745; font-weight: 600; margin-left: 5px;" title="Запрос привязан к товару">🔒</span>' : '';
835
836 tableHTML += `
837 <tr>
838 <td><a href="https://www.ozon.ru/product/${item.sku}" target="_blank" class="ozon-parser-sku-link">${item.sku}</a></td>
839 <td style="max-width: 300px; white-space: normal;">${item.name || '—'}</td>
840 <td><span class="ozon-parser-query-clickable" data-sku="${item.sku}" style="color: #005bff; cursor: pointer; text-decoration: underline;">${item.query}${boundIndicator}</span></td>
841
842 <td style="font-weight: 600;">${Math.round(item.ourCurrentPrice).toLocaleString('ru-RU')} ₽</td>
843 <td>${Math.round(competitorPrice).toLocaleString('ru-RU')} ₽</td>
844 <td style="color: ${currentPriceDelta >= 0 ? '#dc3545' : '#28a745'}; font-weight: 600;">
845 ${currentPriceDelta > 0 ? '+' : ''}${currentPriceDelta.toFixed(1)}%
846 </td>
847
848 <td style="font-weight: 600;">${Math.round(item.ourAvgPrice).toLocaleString('ru-RU')} ₽</td>
849 <td>${Math.round(item.competitorAvgPriceValue).toLocaleString('ru-RU')} ₽</td>
850 <td style="color: ${avgPriceDelta >= 0 ? '#dc3545' : '#28a745'}; font-weight: 600;">
851 ${avgPriceDelta > 0 ? '+' : ''}${avgPriceDelta.toFixed(1)}%
852 </td>
853
854 <td style="font-weight: 600;">${Math.round(item.ourRevenue).toLocaleString('ru-RU')} ₽</td>
855 <td>${Math.round(item.competitorRevenue).toLocaleString('ru-RU')} ₽</td>
856 <td style="color: ${revenueDelta >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600;">
857 ${revenueDelta > 0 ? '+' : ''}${revenueDelta.toFixed(1)}%
858 </td>
859 </tr>
860 `;
861 });
862
863 tableHTML += `
864 </tbody>
865 </table>
866 `;
867
868 const tableContainer = content.querySelector('#prices-table-container');
869 tableContainer.innerHTML = tableHTML;
870
871 // Добавляем обработчики сортировки
872 tableContainer.querySelectorAll('th[data-sort]').forEach(th => {
873 th.addEventListener('click', () => {
874 const column = th.getAttribute('data-sort');
875 if (sortColumn === column) {
876 sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
877 } else {
878 sortColumn = column;
879 sortDirection = 'asc';
880 }
881 displayPricesTable(currentMethod, currentFilters);
882 });
883 });
884
885 // Добавляем обработчики кликов на запросы для выбора привязки
886 tableContainer.querySelectorAll('.ozon-parser-query-clickable').forEach(querySpan => {
887 querySpan.addEventListener('click', async (e) => {
888 e.preventDefault();
889 e.stopPropagation();
890 const sku = querySpan.getAttribute('data-sku');
891 await showQuerySelectionModal(sku, allListResults, skuQueryBindings);
892 });
893 });
894
895 // Добавляем обработчики кликов на строки для открытия панели с деталями
896 tableContainer.querySelectorAll('tbody tr').forEach(row => {
897 row.classList.add('ozon-parser-clickable-row');
898 row.addEventListener('click', async (e) => {
899 // Не открываем панель, если кликнули на ссылку или запрос
900 if (e.target.tagName === 'A' || e.target.classList.contains('ozon-parser-query-clickable')) return;
901
902 const skuLink = row.querySelector('.ozon-parser-sku-link');
903 if (!skuLink) return;
904
905 const sku = skuLink.textContent.trim();
906 const productData = filteredData.find(item => item.sku === sku);
907
908 if (productData) {
909 await showProductDetailsPanel(productData, allListResults);
910 }
911 });
912 });
913 }
914
915 content.innerHTML = `
916 <div class="ozon-parser-modal-header">💰 Анализ цен</div>
917 <div class="ozon-parser-modal-body">
918 <div class="ozon-parser-info">
919 Сравнение цен наших товаров с топ-3 конкурентами по позиции в выдаче
920 </div>
921
922 <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
923 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Метод расчета цены конкурентов:</label>
924 <select class="ozon-parser-search" id="price-method-selector" style="margin-bottom: 0;">
925 <option value="max">Максимум по топ-3</option>
926 <option value="avg">Среднее по топ-3</option>
927 <option value="weighted">Средневзвешенное по выручке</option>
928 <option value="min">Минимальная по топ-3</option>
929 <option value="median">Медиана по топ-3</option>
930 </select>
931 </div>
932
933 <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
934 <label style="font-size: 14px; font-weight: 600; margin-bottom: 12px; display: block;">Фильтры (AND логика):</label>
935
936 <div id="selected-query-info" style="display: none; padding: 10px; background: #e7f3ff; border-left: 4px solid #005bff; border-radius: 4px; margin-bottom: 15px;">
937 <div style="display: flex; justify-content: space-between; align-items: center;">
938 <span style="font-size: 13px; color: #333;">Фильтр по запросу: <strong id="selected-query-text"></strong></span>
939 <button id="clear-query-filter" style="background: #dc3545; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">Очистить</button>
940 </div>
941 </div>
942
943 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 15px;">
944 <div>
945 <label style="font-size: 12px; color: #666; margin-bottom: 5px; display: block;">Поиск по запросу</label>
946 <input type="text" class="ozon-parser-search" id="filter-query-search" placeholder="Введите запрос" style="margin-bottom: 0;">
947 </div>
948 <div>
949 <label style="font-size: 12px; color: #666; margin-bottom: 5px; display: block;">Поиск по SKU</label>
950 <input type="text" class="ozon-parser-search" id="filter-sku-search" placeholder="Введите SKU" style="margin-bottom: 0;">
951 </div>
952 </div>
953
954 <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px;">
955 <div>
956 <label style="font-size: 12px; color: #666; margin-bottom: 5px; display: block;">Выручка</label>
957 <div style="display: flex; gap: 5px;">
958 <select class="ozon-parser-search" id="filter-revenue-direction" style="margin-bottom: 0; flex: 0 0 auto; width: 100px;">
959 <option value="less">Меньше</option>
960 <option value="more">Больше</option>
961 </select>
962 <input type="number" class="ozon-parser-search" id="filter-revenue" placeholder="0" min="0" step="1" value="0" style="margin-bottom: 0; flex: 1;">
963 </div>
964 <div style="font-size: 10px; color: #999; margin-top: 2px;">на % (и более)</div>
965 </div>
966 <div>
967 <label style="font-size: 12px; color: #666; margin-bottom: 5px; display: block;">Текущая цена</label>
968 <div style="display: flex; gap: 5px;">
969 <select class="ozon-parser-search" id="filter-current-price-direction" style="margin-bottom: 0; flex: 0 0 auto; width: 100px;">
970 <option value="more">Больше</option>
971 <option value="less">Меньше</option>
972 </select>
973 <input type="number" class="ozon-parser-search" id="filter-current-price" placeholder="0" min="0" step="1" value="0" style="margin-bottom: 0; flex: 1;">
974 </div>
975 <div style="font-size: 10px; color: #999; margin-top: 2px;">на % (и более)</div>
976 </div>
977 <div>
978 <label style="font-size: 12px; color: #666; margin-bottom: 5px; display: block;">Средняя цена</label>
979 <div style="display: flex; gap: 5px;">
980 <select class="ozon-parser-search" id="filter-avg-price-direction" style="margin-bottom: 0; flex: 0 0 auto; width: 100px;">
981 <option value="more">Больше</option>
982 <option value="less">Меньше</option>
983 </select>
984 <input type="number" class="ozon-parser-search" id="filter-avg-price" placeholder="0" min="0" step="1" value="0" style="margin-bottom: 0; flex: 1;">
985 </div>
986 <div style="font-size: 10px; color: #999; margin-top: 2px;">на % (и более)</div>
987 </div>
988 </div>
989 </div>
990
991 <div id="prices-table-container"></div>
992 </div>
993 <div class="ozon-parser-modal-footer">
994 <button class="ozon-parser-btn secondary" id="export-csv-btn">Экспорт в CSV</button>
995 <button class="ozon-parser-btn" id="send-to-dashboard-btn" style="background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%); margin-right: 10px;">📡 Отправить в дашборд</button>
996 <button class="ozon-parser-btn" id="close-prices-modal">Закрыть</button>
997 </div>
998 `;
999
1000 modal.appendChild(content);
1001 document.body.appendChild(modal);
1002
1003 // Начальное отображение
1004 let currentMethod = 'max';
1005 let currentFilters = {
1006 revenue: 0,
1007 revenueDirection: 'less',
1008 currentPrice: 0,
1009 currentPriceDirection: 'more',
1010 avgPrice: 0,
1011 avgPriceDirection: 'more',
1012 querySearch: '',
1013 skuSearch: '',
1014 selectedQuery: ''
1015 };
1016
1017 // Загружаем сохраненный запрос
1018 const savedQuery = await GM.getValue('ozon_parser_selected_query', '');
1019 if (savedQuery) {
1020 currentFilters.selectedQuery = savedQuery;
1021 }
1022
1023 displayPricesTable(currentMethod, currentFilters);
1024
1025 // Обновляем информационную панель после первого рендера
1026 if (savedQuery) {
1027 const selectedQueryInfo = content.querySelector('#selected-query-info');
1028 const selectedQueryText = content.querySelector('#selected-query-text');
1029 if (selectedQueryInfo && selectedQueryText) {
1030 selectedQueryInfo.style.display = 'block';
1031 selectedQueryText.textContent = savedQuery;
1032 }
1033 }
1034
1035 // Функция для обновления фильтров из полей ввода
1036 function updateFiltersFromInputs() {
1037 currentFilters = {
1038 revenue: parseFloat(content.querySelector('#filter-revenue').value) || 0,
1039 revenueDirection: content.querySelector('#filter-revenue-direction').value,
1040 currentPrice: parseFloat(content.querySelector('#filter-current-price').value) || 0,
1041 currentPriceDirection: content.querySelector('#filter-current-price-direction').value,
1042 avgPrice: parseFloat(content.querySelector('#filter-avg-price').value) || 0,
1043 avgPriceDirection: content.querySelector('#filter-avg-price-direction').value,
1044 querySearch: content.querySelector('#filter-query-search').value.trim(),
1045 skuSearch: content.querySelector('#filter-sku-search').value.trim(),
1046 selectedQuery: currentFilters.selectedQuery
1047 };
1048 displayPricesTable(currentMethod, currentFilters);
1049 }
1050
1051 // Обработчик очистки фильтра по запросу
1052 const clearQueryFilterBtn = content.querySelector('#clear-query-filter');
1053 if (clearQueryFilterBtn) {
1054 clearQueryFilterBtn.addEventListener('click', async () => {
1055 currentFilters.selectedQuery = '';
1056 await GM.setValue('ozon_parser_selected_query', '');
1057 const selectedQueryInfo = content.querySelector('#selected-query-info');
1058 if (selectedQueryInfo) {
1059 selectedQueryInfo.style.display = 'none';
1060 }
1061 displayPricesTable(currentMethod, currentFilters);
1062 });
1063 }
1064
1065 // Обработчик изменения метода расчета
1066 const methodSelector = content.querySelector('#price-method-selector');
1067 methodSelector.addEventListener('change', () => {
1068 currentMethod = methodSelector.value;
1069 displayPricesTable(currentMethod, currentFilters);
1070 });
1071
1072 // Обработчики для всех фильтров - применяем автоматически
1073 content.querySelector('#filter-query-search').addEventListener('input', debounce(updateFiltersFromInputs, 300));
1074 content.querySelector('#filter-sku-search').addEventListener('input', debounce(updateFiltersFromInputs, 300));
1075
1076 content.querySelector('#filter-revenue').addEventListener('input', debounce(updateFiltersFromInputs, 500));
1077 content.querySelector('#filter-revenue-direction').addEventListener('change', updateFiltersFromInputs);
1078
1079 content.querySelector('#filter-current-price').addEventListener('input', debounce(updateFiltersFromInputs, 500));
1080 content.querySelector('#filter-current-price-direction').addEventListener('change', updateFiltersFromInputs);
1081
1082 content.querySelector('#filter-avg-price').addEventListener('input', debounce(updateFiltersFromInputs, 500));
1083 content.querySelector('#filter-avg-price-direction').addEventListener('change', updateFiltersFromInputs);
1084
1085 // Обработчик экспорта в CSV
1086 content.querySelector('#export-csv-btn').addEventListener('click', () => {
1087 exportToCSV(ourProductsData, currentMethod, currentFilters);
1088 });
1089 // Обработчик отправки в дашборд
1090content.querySelector('#send-to-dashboard-btn').addEventListener('click', async () => {
1091 const btn = content.querySelector('#send-to-dashboard-btn');
1092 btn.disabled = true;
1093 btn.textContent = '⏳ Отправка...';
1094
1095 try {
1096 // Формируем данные для отправки
1097 const dataToSend = ourProductsData.map(item => {
1098 // Получаем текущий метод из селектора
1099 const methodSelector = content.querySelector('#price-method-selector');
1100 const selectedMethod = methodSelector ? methodSelector.value : 'max';
1101
1102 // Выбираем цену конкурента по методу
1103 let competitorPriceCurrent;
1104 switch(selectedMethod) {
1105 case 'max': competitorPriceCurrent = item.competitorMaxPrice; break;
1106 case 'avg': competitorPriceCurrent = item.competitorAvgPrice; break;
1107 case 'weighted': competitorPriceCurrent = item.competitorWeightedPrice; break;
1108 case 'min': competitorPriceCurrent = item.competitorMinPrice; break;
1109 case 'median': competitorPriceCurrent = item.competitorMedianPrice; break;
1110 default: competitorPriceCurrent = item.competitorMaxPrice;
1111 }
1112
1113 return {
1114 sku: String(item.sku),
1115 title: String(item.name || 'Без названия'),
1116 query: String(item.query || 'неизвестно'),
1117 list: String(item.listName || 'Без названия'),
1118 our_price_current: Math.round(item.ourCurrentPrice || 0),
1119 competitor_price_current: Math.round(competitorPriceCurrent || 0),
1120 delta_current_percent: parseFloat((((item.ourCurrentPrice - competitorPriceCurrent) / Math.max(competitorPriceCurrent || 1, 1)) * 100).toFixed(1)),
1121 our_price_avg: Math.round(item.ourAvgPrice || 0),
1122 competitor_price_avg: Math.round(item.competitorAvgPriceValue || item.competitorAvgPrice || 0),
1123 delta_avg_percent: parseFloat((((item.ourAvgPrice - (item.competitorAvgPriceValue || item.competitorAvgPrice || 0)) / Math.max(item.competitorAvgPriceValue || item.competitorAvgPrice || 1, 1)) * 100).toFixed(1)),
1124 our_revenue: Math.round(item.ourRevenue || 0),
1125 competitor_revenue: Math.round(item.competitorRevenue || 0),
1126 delta_revenue_percent: parseFloat((((item.ourRevenue - item.competitorRevenue) / Math.max(item.competitorRevenue || 1, 1)) * 100).toFixed(1)),
1127 parsed_at: new Date().toISOString(),
1128 method_used: selectedMethod // для отладки
1129 };
1130});
1131
1132 // Отправляем на Blink
1133 const response = await fetch('https://8p6h747w--products.functions.blink.new/products', {
1134 method: 'POST',
1135 headers: {
1136 'Content-Type': 'application/json',
1137 'X-API-Key': 'secret123'
1138 },
1139 body: JSON.stringify(dataToSend)
1140 });
1141
1142 if (!response.ok) {
1143 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1144 }
1145
1146 const result = await response.json();
1147 btn.textContent = '✅ Отправлено!';
1148 btn.style.background = 'linear-gradient(135deg, #00b894 0%, #00cec9 100%)';
1149
1150 alert(`Успешно отправлено ${dataToSend.length} товаров в дашборд!\n\nОтвет сервера: ${JSON.stringify(result, null, 2)}`);
1151
1152 setTimeout(() => {
1153 btn.disabled = false;
1154 btn.textContent = '📡 Отправить в дашборд';
1155 btn.style.background = 'linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%)';
1156 }, 3000);
1157
1158 } catch (error) {
1159 console.error('Ozon Product Parser: Error sending to dashboard:', error);
1160 btn.textContent = '❌ Ошибка';
1161 btn.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
1162
1163 alert('Ошибка при отправке в дашборд:\n\n' + error.message + '\n\nПроверьте:\n1. URL дашборда (возможно он изменился)\n2. Подключение к интернету\n3. CORS политику (для разработки)');
1164
1165 setTimeout(() => {
1166 btn.disabled = false;
1167 btn.textContent = '📡 Отправить в дашборд';
1168 btn.style.background = 'linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%)';
1169 }, 3000);
1170 }
1171});
1172
1173 // Обработчик закрытия
1174 content.querySelector('#close-prices-modal').addEventListener('click', () => {
1175 modal.remove();
1176 });
1177
1178 modal.addEventListener('click', (e) => {
1179 if (e.target === modal) {
1180 modal.remove();
1181 }
1182 });
1183
1184 console.log('Ozon Product Parser: Prices modal shown');
1185 } catch (error) {
1186 console.error('Ozon Product Parser: Error in showPricesModal:', error);
1187 alert('Ошибка при отображении цен: ' + error.message);
1188 }
1189 }
1190
1191 // Показываем модальное окно выбора запроса для товара
1192 async function showQuerySelectionModal(sku, allListResults, skuQueryBindings) {
1193 console.log('Ozon Product Parser: Opening query selection modal for SKU:', sku);
1194
1195 // Собираем все запросы, где встречается этот товар
1196 const queriesWithProduct = [];
1197 for (const listName in allListResults) {
1198 const listData = allListResults[listName];
1199 for (const query in listData.queries) {
1200 const products = listData.queries[query];
1201 const product = products.find(p => p.sku === sku);
1202 if (product) {
1203 // Находим конкурентов для этого запроса
1204 const competitors = products.filter(p => !p.isTargetBrand && p.price > 0);
1205 const sortedCompetitors = [...competitors].sort((a, b) => a.position - b.position);
1206 const top3Competitors = sortedCompetitors.slice(0, 3);
1207
1208 if (top3Competitors.length > 0) {
1209 const competitorPrices = top3Competitors.map(c => c.price);
1210 const competitorRevenues = top3Competitors.map(c => c.revenue);
1211
1212 const competitorMaxPrice = Math.max(...competitorPrices);
1213 const competitorAvgPrice = competitorPrices.reduce((sum, p) => sum + p, 0) / competitorPrices.length;
1214 const competitorRevenue = competitorRevenues.reduce((sum, r) => sum + r, 0) / competitorRevenues.length;
1215
1216 queriesWithProduct.push({
1217 listName: listName,
1218 query: query,
1219 product: product,
1220 competitorMaxPrice: competitorMaxPrice,
1221 competitorAvgPrice: competitorAvgPrice,
1222 competitorRevenue: competitorRevenue
1223 });
1224 }
1225 }
1226 }
1227 }
1228
1229 if (queriesWithProduct.length === 0) {
1230 alert('Товар не найден ни в одном запросе');
1231 return;
1232 }
1233
1234 const modal = document.createElement('div');
1235 modal.className = 'ozon-parser-modal';
1236 modal.style.zIndex = '10003';
1237
1238 const content = document.createElement('div');
1239 content.className = 'ozon-parser-modal-content';
1240 content.style.maxWidth = '900px';
1241
1242 const currentBoundQuery = skuQueryBindings[sku];
1243
1244 let modalHTML = `
1245 <div class="ozon-parser-modal-header">Выбор запроса для SKU ${sku}</div>
1246 <div class="ozon-parser-modal-body">
1247 <div class="ozon-parser-info">
1248 Нажмите на значок замка, чтобы закрепить запрос для этого товара.
1249 ${currentBoundQuery ? `<br><strong>Текущая привязка:</strong> ${currentBoundQuery}` : ''}
1250 </div>
1251 <table class="ozon-parser-results-table">
1252 <thead>
1253 <tr>
1254 <th style="width: 40px;"></th>
1255 <th>Запрос</th>
1256 <th>Список</th>
1257 <th>Позиция</th>
1258 <th>Выручка товара</th>
1259 <th>Макс. цена конкурентов</th>
1260 <th>Средняя цена конкурентов</th>
1261 <th>Средняя выручка конкурентов</th>
1262 </tr>
1263 </thead>
1264 <tbody>
1265 `;
1266
1267 queriesWithProduct.forEach(item => {
1268 const isBound = currentBoundQuery === item.query;
1269 const rowStyle = isBound ? 'background: #d4edda;' : '';
1270 const lockIcon = isBound ? '🔒' : '🔓';
1271 const lockColor = isBound ? '#28a745' : '#999';
1272
1273 modalHTML += `
1274 <tr style="${rowStyle}">
1275 <td style="text-align: center;">
1276 <span class="query-lock-icon" data-query="${item.query}" style="cursor: pointer; font-size: 20px; color: ${lockColor}; user-select: none;" title="${isBound ? 'Отвязать запрос' : 'Закрепить запрос'}">${lockIcon}</span>
1277 </td>
1278 <td style="font-weight: ${isBound ? '600' : 'normal'};">${item.query}</td>
1279 <td>${item.listName}</td>
1280 <td>${item.product.position}</td>
1281 <td>${Math.round(item.product.revenue).toLocaleString('ru-RU')} ₽</td>
1282 <td>${Math.round(item.competitorMaxPrice).toLocaleString('ru-RU')} ₽</td>
1283 <td>${Math.round(item.competitorAvgPrice).toLocaleString('ru-RU')} ₽</td>
1284 <td>${Math.round(item.competitorRevenue).toLocaleString('ru-RU')} ₽</td>
1285 </tr>
1286 `;
1287 });
1288
1289 modalHTML += `
1290 </tbody>
1291 </table>
1292 </div>
1293 <div class="ozon-parser-modal-footer">
1294 <button class="ozon-parser-btn" id="close-query-selection-modal">Закрыть</button>
1295 </div>
1296 `;
1297
1298 content.innerHTML = modalHTML;
1299 modal.appendChild(content);
1300 document.body.appendChild(modal);
1301
1302 // Обработчики кликов на замки
1303 content.querySelectorAll('.query-lock-icon').forEach(lockIcon => {
1304 lockIcon.addEventListener('click', async () => {
1305 const selectedQuery = lockIcon.getAttribute('data-query');
1306 const isBound = currentBoundQuery === selectedQuery;
1307
1308 if (isBound) {
1309 // Отвязываем запрос
1310 if (confirm(`Отвязать запрос "${selectedQuery}" от товара ${sku}?`)) {
1311 delete skuQueryBindings[sku];
1312 await GM.setValue('ozon_parser_sku_query_bindings', JSON.stringify(skuQueryBindings));
1313
1314 console.log(`Ozon Product Parser: Unbound SKU ${sku} from query "${selectedQuery}"`);
1315
1316 modal.remove();
1317
1318 // Перезагружаем таблицу цен
1319 await showPricesModal();
1320 }
1321 } else {
1322 // Привязываем запрос
1323 skuQueryBindings[sku] = selectedQuery;
1324 await GM.setValue('ozon_parser_sku_query_bindings', JSON.stringify(skuQueryBindings));
1325
1326 console.log(`Ozon Product Parser: Bound SKU ${sku} to query "${selectedQuery}"`);
1327
1328 modal.remove();
1329
1330 // Перезагружаем таблицу цен
1331 await showPricesModal();
1332 }
1333 });
1334 });
1335
1336 // Закрытие
1337 content.querySelector('#close-query-selection-modal').addEventListener('click', () => {
1338 modal.remove();
1339 });
1340
1341 modal.addEventListener('click', (e) => {
1342 if (e.target === modal) {
1343 modal.remove();
1344 }
1345 });
1346 }
1347
1348 // Показываем панель с деталями товара
1349 async function showProductDetailsPanel(productData, allListResults) {
1350 console.log('Ozon Product Parser: Opening product details panel for SKU:', productData.sku);
1351
1352 // Удаляем существующую панель, если она есть
1353 const existingPanel = document.querySelector('.ozon-parser-details-panel');
1354 if (existingPanel) {
1355 existingPanel.remove();
1356 }
1357
1358 // Создаем панель
1359 const panel = document.createElement('div');
1360 panel.className = 'ozon-parser-details-panel';
1361
1362 // Загружаем привязки SKU к запросам
1363 const skuQueryBindingsJson = await GM.getValue('ozon_parser_sku_query_bindings', '{}');
1364 const skuQueryBindings = JSON.parse(skuQueryBindingsJson);
1365 const boundQuery = skuQueryBindings[productData.sku];
1366
1367 // Собираем все запросы, где встречается этот товар
1368 const queriesWithProduct = [];
1369 for (const listName in allListResults) {
1370 const listData = allListResults[listName];
1371 for (const query in listData.queries) {
1372 const products = listData.queries[query];
1373 const product = products.find(p => p.sku === productData.sku);
1374 if (product) {
1375 // Если есть привязка и это не тот запрос - пропускаем
1376 if (boundQuery && boundQuery !== query) {
1377 continue;
1378 }
1379
1380 queriesWithProduct.push({
1381 listName: listName,
1382 query: query,
1383 product: product,
1384 allProducts: products
1385 });
1386 }
1387 }
1388 }
1389
1390 // Функция для генерации HTML панели с аналитикой
1391 async function generatePanelHTML(queryData) {
1392 const analytics = await analyzeProducts(queryData.allProducts);
1393
1394 return `
1395 <div class="ozon-parser-details-panel-header">
1396 <div class="ozon-parser-details-panel-title">SKU: ${productData.sku}</div>
1397 <button class="ozon-parser-details-panel-close">Закрыть</button>
1398 </div>
1399 <div class="ozon-parser-details-panel-body">
1400 <div style="margin-bottom: 20px;">
1401 <div style="font-size: 16px; font-weight: 600; margin-bottom: 10px;">${productData.name || 'Название не найдено'}</div>
1402 <div style="font-size: 14px; color: #666; margin-bottom: 5px;">Текущая цена: <strong>${Math.round(productData.ourCurrentPrice).toLocaleString('ru-RU')} ₽</strong></div>
1403 <div style="font-size: 14px; color: #666; margin-bottom: 5px;">Средняя цена: <strong>${Math.round(productData.ourAvgPrice).toLocaleString('ru-RU')} ₽</strong></div>
1404 <div style="font-size: 14px; color: #666;">Выручка: <strong>${Math.round(productData.ourRevenue).toLocaleString('ru-RU')} ₽</strong></div>
1405 </div>
1406
1407 ${analytics && analytics.elasticity ? `
1408 <div style="margin-bottom: 20px; padding: 15px; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #005bff;">
1409 <div style="font-size: 15px; font-weight: 600; margin-bottom: 10px; color: #005bff;">📊 Эластичность запроса</div>
1410 <div style="font-size: 24px; font-weight: 700; color: #333; margin-bottom: 8px;">${analytics.elasticity.value}</div>
1411 <div style="font-size: 13px; color: #666; line-height: 1.5;">
1412 ${analytics.elasticity.interpretation}
1413 </div>
1414 </div>
1415 ` : ''}
1416
1417 ${analytics && analytics.recommendedPrices ? `
1418 <div style="margin-bottom: 20px;">
1419 <div style="font-size: 15px; font-weight: 600; margin-bottom: 12px; color: #005bff;">💰 Стратегии ценообразования</div>
1420
1421 <div style="background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #6c757d;">
1422 <div style="font-size: 14px; font-weight: 600; color: #6c757d; margin-bottom: 5px;">⚡ Захват рынка</div>
1423 <div style="font-size: 20px; font-weight: 700; color: #333; margin-bottom: 5px;">${analytics.recommendedPrices.marketCapture.price.toLocaleString('ru-RU')} ₽</div>
1424 <div style="font-size: 12px; color: #666; margin-bottom: 8px;">${analytics.recommendedPrices.marketCapture.description}</div>
1425 ${analytics.brandRecommendations && (analytics.brandRecommendations.gls || analytics.brandRecommendations.skinphoria) ? `
1426 <div style="font-size: 11px; color: #666; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
1427 ${(() => {
1428 const ourProduct = (analytics.brandRecommendations.gls?.currentProducts || [])
1429 .concat(analytics.brandRecommendations.skinphoria?.currentProducts || [])
1430 .find(p => p.sku === productData.sku);
1431 if (!ourProduct || !ourProduct.recommendations || !ourProduct.recommendations.forecast || !ourProduct.recommendations.forecast.marketCapture) return '';
1432 const forecast = ourProduct.recommendations.forecast.marketCapture;
1433 return `
1434 <div style="color: ${forecast.revenueChange >= 0 ? '#28a745' : '#dc3545'};">Выручка: ${forecast.revenueChange > 0 ? '+' : ''}${forecast.revenueChange}%</div>
1435 <div style="color: ${forecast.ordersChange >= 0 ? '#28a745' : '#dc3545'};">Заказы: ${forecast.ordersChange > 0 ? '+' : ''}${forecast.ordersChange}%</div>
1436 ${forecast.profit !== null ? `<div style="color: ${forecast.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600;">Прибыль: ${forecast.profitChange > 0 ? '+' : ''}${forecast.profitChange}% (${forecast.profit.toFixed(0)} ₽)</div>` : ''}
1437 `;
1438 })()}
1439 </div>
1440 ` : ''}
1441 </div>
1442
1443 <div style="background: #fff3cd; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid #dc3545;">
1444 <div style="font-size: 14px; font-weight: 600; color: #dc3545; margin-bottom: 5px;">✅ Оптимальная</div>
1445 <div style="font-size: 20px; font-weight: 700; color: #333; margin-bottom: 5px;">${analytics.recommendedPrices.aggressive.price.toLocaleString('ru-RU')} ₽</div>
1446 <div style="font-size: 12px; color: #666; margin-bottom: 8px;">${analytics.recommendedPrices.aggressive.description}</div>
1447 ${analytics.brandRecommendations && (analytics.brandRecommendations.gls || analytics.brandRecommendations.skinphoria) ? `
1448 <div style="font-size: 11px; color: #666; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
1449 ${(() => {
1450 const ourProduct = (analytics.brandRecommendations.gls?.currentProducts || [])
1451 .concat(analytics.brandRecommendations.skinphoria?.currentProducts || [])
1452 .find(p => p.sku === productData.sku);
1453 if (!ourProduct || !ourProduct.recommendations || !ourProduct.recommendations.forecast || !ourProduct.recommendations.forecast.aggressive) return '';
1454 const forecast = ourProduct.recommendations.forecast.aggressive;
1455 return `
1456 <div style="color: ${forecast.revenueChange >= 0 ? '#28a745' : '#dc3545'};">Выручка: ${forecast.revenueChange > 0 ? '+' : ''}${forecast.revenueChange}%</div>
1457 <div style="color: ${forecast.ordersChange >= 0 ? '#28a745' : '#dc3545'};">Заказы: ${forecast.ordersChange > 0 ? '+' : ''}${forecast.ordersChange}%</div>
1458 ${forecast.profit !== null ? `<div style="color: ${forecast.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600;">Прибыль: ${forecast.profitChange > 0 ? '+' : ''}${forecast.profitChange}% (${forecast.profit.toFixed(0)} ₽)</div>` : ''}
1459 `;
1460 })()}
1461 </div>
1462 ` : ''}
1463 </div>
1464
1465 <div style="background: #d4edda; padding: 12px; border-radius: 8px; border-left: 4px solid #28a745;">
1466 <div style="font-size: 14px; font-weight: 600; color: #28a745; margin-bottom: 5px;">🔥 Агрессивная</div>
1467 <div style="font-size: 20px; font-weight: 700; color: #333; margin-bottom: 5px;">${analytics.recommendedPrices.optimal.price.toLocaleString('ru-RU')} ₽</div>
1468 <div style="font-size: 12px; color: #666; margin-bottom: 8px;">${analytics.recommendedPrices.optimal.description}</div>
1469 ${analytics.brandRecommendations && (analytics.brandRecommendations.gls || analytics.brandRecommendations.skinphoria) ? `
1470 <div style="font-size: 11px; color: #666; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e0e0e0;">
1471 ${(() => {
1472 const ourProduct = (analytics.brandRecommendations.gls?.currentProducts || [])
1473 .concat(analytics.brandRecommendations.skinphoria?.currentProducts || [])
1474 .find(p => p.sku === productData.sku);
1475 if (!ourProduct || !ourProduct.recommendations || !ourProduct.recommendations.forecast || !ourProduct.recommendations.forecast.optimal) return '';
1476 const forecast = ourProduct.recommendations.forecast.optimal;
1477 return `
1478 <div style="color: ${forecast.revenueChange >= 0 ? '#28a745' : '#dc3545'};">Выручка: ${forecast.revenueChange > 0 ? '+' : ''}${forecast.revenueChange}%</div>
1479 <div style="color: ${forecast.ordersChange >= 0 ? '#28a745' : '#dc3545'};">Заказы: ${forecast.ordersChange > 0 ? '+' : ''}${forecast.ordersChange}%</div>
1480 ${forecast.profit !== null ? `<div style="color: ${forecast.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600;">Прибыль: ${forecast.profitChange > 0 ? '+' : ''}${forecast.profitChange}% (${forecast.profit.toFixed(0)} ₽)</div>` : ''}
1481 `;
1482 })()}
1483 </div>
1484 ` : ''}
1485 </div>
1486 </div>
1487 ` : ''}
1488
1489 <div style="margin-bottom: 20px;">
1490 <div style="font-size: 15px; font-weight: 600; margin-bottom: 10px; color: #005bff;">Запросы с этим товаром (${queriesWithProduct.length})</div>
1491 <select class="ozon-parser-search" id="query-selector-panel" style="margin-bottom: 15px;">
1492 ${queriesWithProduct.map((item, index) => `
1493 <option value="${index}">${item.query} (${item.listName})</option>
1494 `).join('')}
1495 </select>
1496 <div id="query-details-container"></div>
1497 </div>
1498 </div>
1499 `;
1500 }
1501
1502 // Получаем аналитику для первого запроса и формируем HTML
1503 const firstQueryData = queriesWithProduct[0];
1504 const initialHTML = await generatePanelHTML(firstQueryData);
1505
1506 panel.innerHTML = initialHTML;
1507 document.body.appendChild(panel);
1508
1509 // Функция для отображения деталей выбранного запроса
1510 function displayQueryDetails(queryIndex) {
1511 const queryData = queriesWithProduct[queryIndex];
1512 const product = queryData.product;
1513 const allProducts = queryData.allProducts;
1514 const query = queryData.query;
1515
1516 // Находим топ-3 конкурентов по выручке
1517 const competitors = allProducts.filter(p => !p.isTargetBrand && p.price > 0 && p.revenue > 0);
1518 const sortedCompetitors = [...competitors].sort((a, b) => b.revenue - a.revenue);
1519 const top3Competitors = sortedCompetitors.slice(0, 3);
1520
1521 // Позиция товара по выручке
1522 const sortedByRevenue = [...allProducts].sort((a, b) => b.revenue - a.revenue);
1523 const revenuePosition = sortedByRevenue.findIndex(p => p.sku === product.sku) + 1;
1524
1525 // URL для поиска на Ozon
1526 const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(query)}&from_global=true`;
1527
1528 let detailsHTML = `
1529 <div style="padding: 15px; background: #f8f9fa; border-radius: 8px; margin-bottom: 15px;">
1530 <div style="font-size: 14px; font-weight: 600; margin-bottom: 10px;">Позиции в выдаче</div>
1531 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
1532 <div>
1533 <div style="font-size: 12px; color: #666;">По позиции</div>
1534 <div style="font-size: 18px; font-weight: 700; color: #005bff;">${product.position} место</div>
1535 </div>
1536 <div>
1537 <div style="font-size: 12px; color: #666;">По выручке</div>
1538 <div style="font-size: 18px; font-weight: 700; color: #005bff;">${revenuePosition} место</div>
1539 </div>
1540 </div>
1541 <div style="margin-top: 10px;">
1542 <a href="${searchUrl}" target="_blank" class="ozon-parser-sku-link" style="font-size: 13px;">🔍 Открыть запрос на Ozon</a>
1543 </div>
1544 </div>
1545
1546 <div style="padding: 15px; background: #f8f9fa; border-radius: 8px; margin-bottom: 15px;">
1547 <div style="font-size: 14px; font-weight: 600; margin-bottom: 10px;">Метрики товара</div>
1548 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Цена: <strong>${product.price.toLocaleString('ru-RU')} ₽</strong></div>
1549 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Выручка: <strong>${product.revenue.toLocaleString('ru-RU')} ₽</strong></div>
1550 <div style="font-size: 13px; color: #666;">Заказы: <strong>${product.orders.toLocaleString('ru-RU')}</strong></div>
1551 </div>
1552 `;
1553
1554 if (top3Competitors.length > 0) {
1555 detailsHTML += `
1556 <div style="padding: 15px; background: #fff3cd; border-radius: 8px;">
1557 <div style="font-size: 14px; font-weight: 600; margin-bottom: 10px;">Топ-3 конкурента по выручке</div>
1558 <table class="ozon-parser-results-table" style="margin-top: 0;">
1559 <thead>
1560 <tr>
1561 <th>Позиция</th>
1562 <th>Бренд</th>
1563 <th>Текущая цена</th>
1564 <th>Средняя цена</th>
1565 <th>Выручка</th>
1566 <th>Заказы</th>
1567 </tr>
1568 </thead>
1569 <tbody>
1570 `;
1571
1572 top3Competitors.forEach(comp => {
1573 const avgPrice = comp.orders > 0 ? comp.revenue / comp.orders : 0;
1574 detailsHTML += `
1575 <tr>
1576 <td>${comp.position}</td>
1577 <td><a href="https://www.ozon.ru/product/${comp.sku}" target="_blank" class="ozon-parser-sku-link">${comp.brand || '—'}</a></td>
1578 <td>${comp.price.toLocaleString('ru-RU')} ₽</td>
1579 <td>${Math.round(avgPrice).toLocaleString('ru-RU')} ₽</td>
1580 <td>${comp.revenue.toLocaleString('ru-RU')} ₽</td>
1581 <td>${comp.orders.toLocaleString('ru-RU')}</td>
1582 </tr>
1583 `;
1584 });
1585
1586 detailsHTML += `
1587 </tbody>
1588 </table>
1589 </div>
1590 `;
1591 }
1592
1593 const container = panel.querySelector('#query-details-container');
1594 container.innerHTML = detailsHTML;
1595 }
1596
1597 // Функция для обновления панели при смене запроса
1598 async function updatePanelForQuery(queryIndex) {
1599 const selectedQueryData = queriesWithProduct[queryIndex];
1600
1601 // Пересчитываем аналитику для выбранного запроса
1602 const newHTML = await generatePanelHTML(selectedQueryData);
1603 panel.innerHTML = newHTML;
1604
1605 // Переподключаем обработчики после обновления HTML
1606 const newQuerySelector = panel.querySelector('#query-selector-panel');
1607 newQuerySelector.value = queryIndex;
1608
1609 // Переподключаем обработчик изменения запроса
1610 newQuerySelector.addEventListener('change', async () => {
1611 const newSelectedIndex = parseInt(newQuerySelector.value);
1612 await updatePanelForQuery(newSelectedIndex);
1613 });
1614
1615 // Переподключаем обработчик закрытия
1616 const newCloseBtn = panel.querySelector('.ozon-parser-details-panel-close');
1617 newCloseBtn.addEventListener('click', () => {
1618 panel.classList.remove('open');
1619 setTimeout(() => panel.remove(), 300);
1620 });
1621
1622 // Отображаем детали выбранного запроса
1623 displayQueryDetails(queryIndex);
1624 }
1625
1626 // Отображаем детали первого запроса
1627 displayQueryDetails(0);
1628
1629 // Обработчик изменения запроса
1630 const querySelector = panel.querySelector('#query-selector-panel');
1631 querySelector.addEventListener('change', async () => {
1632 const selectedIndex = parseInt(querySelector.value);
1633 await updatePanelForQuery(selectedIndex);
1634 });
1635
1636 // Обработчик закрытия панели
1637 const closeBtn = panel.querySelector('.ozon-parser-details-panel-close');
1638 closeBtn.addEventListener('click', () => {
1639 panel.classList.remove('open');
1640 setTimeout(() => panel.remove(), 300);
1641 });
1642
1643 // Открываем панель с анимацией
1644 setTimeout(() => {
1645 panel.classList.add('open');
1646 }, 10);
1647
1648 console.log('Ozon Product Parser: Product details panel opened');
1649 }
1650
1651 // Создаем UI кнопок
1652 function createUI() {
1653 const container = document.createElement('div');
1654 container.className = 'ozon-parser-container';
1655
1656 // Загружаем сохраненную позицию
1657 GM.getValue('ozon_parser_position', JSON.stringify({ top: 20, right: 20 })).then(posJson => {
1658 const pos = JSON.parse(posJson);
1659 container.style.top = pos.top + 'px';
1660 container.style.right = pos.right + 'px';
1661 });
1662
1663 // Загружаем состояние сворачивания
1664 GM.getValue('ozon_parser_collapsed', 'false').then(collapsed => {
1665 if (collapsed === 'true') {
1666 container.classList.add('collapsed');
1667 buttonsWrapper.classList.add('hidden');
1668 }
1669 });
1670
1671 // Кнопка сворачивания/разворачивания
1672 const toggleBtn = document.createElement('button');
1673 toggleBtn.className = 'ozon-parser-toggle-btn';
1674 toggleBtn.innerHTML = '📊';
1675 toggleBtn.title = 'Свернуть/Развернуть панель';
1676
1677 // Обертка для кнопок
1678 const buttonsWrapper = document.createElement('div');
1679 buttonsWrapper.className = 'ozon-parser-buttons-wrapper';
1680
1681 const parseBtn = document.createElement('button');
1682 parseBtn.className = 'ozon-parser-btn';
1683 parseBtn.textContent = 'Парсинг';
1684 parseBtn.addEventListener('click', (e) => {
1685 e.stopPropagation();
1686 showParseModal();
1687 });
1688
1689 const pricesBtn = document.createElement('button');
1690 pricesBtn.className = 'ozon-parser-btn';
1691 pricesBtn.id = 'prices-btn';
1692 pricesBtn.style.background = 'linear-gradient(135deg, #dc3545 0%, #c82333 100%)';
1693 pricesBtn.style.boxShadow = '0 4px 12px rgba(220, 53, 69, 0.3)';
1694 pricesBtn.textContent = 'Цены';
1695 pricesBtn.addEventListener('click', (e) => {
1696 e.stopPropagation();
1697 showPricesModal();
1698 });
1699
1700 const resultsBtn = document.createElement('button');
1701 resultsBtn.className = 'ozon-parser-btn';
1702 resultsBtn.id = 'results-btn';
1703 resultsBtn.style.background = 'linear-gradient(135deg, #28a745 0%, #1e7e34 100%)';
1704 resultsBtn.style.boxShadow = '0 4px 12px rgba(40, 167, 69, 0.3)';
1705 resultsBtn.textContent = 'Результаты';
1706 resultsBtn.addEventListener('click', (e) => {
1707 e.stopPropagation();
1708 showResultsModal();
1709 });
1710
1711 buttonsWrapper.appendChild(parseBtn);
1712 buttonsWrapper.appendChild(pricesBtn);
1713 buttonsWrapper.appendChild(resultsBtn);
1714
1715 container.appendChild(toggleBtn);
1716 container.appendChild(buttonsWrapper);
1717
1718 // Обработчик сворачивания/разворачивания
1719 toggleBtn.addEventListener('click', async (e) => {
1720 e.stopPropagation();
1721 const isCollapsed = container.classList.toggle('collapsed');
1722 buttonsWrapper.classList.toggle('hidden');
1723 await GM.setValue('ozon_parser_collapsed', isCollapsed ? 'true' : 'false');
1724 });
1725
1726 // Перетаскивание панели
1727 let isDragging = false;
1728 let currentX;
1729 let currentY;
1730 let initialX;
1731 let initialY;
1732
1733 container.addEventListener('mousedown', (e) => {
1734 // Не начинаем перетаскивание, если кликнули на кнопку
1735 if (e.target.tagName === 'BUTTON') return;
1736
1737 isDragging = true;
1738 initialX = e.clientX - container.offsetLeft;
1739 initialY = e.clientY - container.offsetTop;
1740 container.style.cursor = 'grabbing';
1741 });
1742
1743 document.addEventListener('mousemove', (e) => {
1744 if (!isDragging) return;
1745
1746 e.preventDefault();
1747 currentX = e.clientX - initialX;
1748 currentY = e.clientY - initialY;
1749
1750 // Ограничиваем перемещение в пределах окна
1751 const maxX = window.innerWidth - container.offsetWidth;
1752 const maxY = window.innerHeight - container.offsetHeight;
1753
1754 currentX = Math.max(0, Math.min(currentX, maxX));
1755 currentY = Math.max(0, Math.min(currentY, maxY));
1756
1757 container.style.left = currentX + 'px';
1758 container.style.top = currentY + 'px';
1759 container.style.right = 'auto';
1760 });
1761
1762 document.addEventListener('mouseup', async () => {
1763 if (isDragging) {
1764 isDragging = false;
1765 container.style.cursor = 'move';
1766
1767 // Сохраняем позицию
1768 const rect = container.getBoundingClientRect();
1769 await GM.setValue('ozon_parser_position', JSON.stringify({
1770 top: rect.top,
1771 right: window.innerWidth - rect.right
1772 }));
1773 }
1774 });
1775
1776 document.body.appendChild(container);
1777 console.log('Ozon Product Parser: UI created');
1778 }
1779
1780 // Показываем модальное окно для ввода запросов
1781 function showParseModal() {
1782 const modal = document.createElement('div');
1783 modal.className = 'ozon-parser-modal';
1784
1785 const content = document.createElement('div');
1786 content.className = 'ozon-parser-modal-content';
1787 content.innerHTML = `
1788 <div class="ozon-parser-modal-header">Парсинг товаров Ozon</div>
1789 <div class="ozon-parser-modal-body">
1790 <div class="ozon-parser-info">
1791 Введите поисковые запросы (каждый с новой строки). Парсер извлечет топ-16 товаров для каждого запроса.
1792 </div>
1793 <div style="margin-bottom: 15px;">
1794 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Скидка Ozon (%):</label>
1795 <div style="display: flex; gap: 10px; align-items: center;">
1796 <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;">
1797 <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>
1798 </div>
1799 <div style="font-size: 12px; color: #666; margin-top: 5px;">
1800 Укажите среднюю скидку Ozon для расчета базовой цены (цены поручения). Например, если товар продается по 500₽ со скидкой 50%, то базовая цена = 1000₽.
1801 </div>
1802 </div>
1803 <div style="margin-bottom: 15px;">
1804 <button class="ozon-parser-btn" id="load-list-btn" style="width: 100%; margin-bottom: 10px;">Загрузить сохраненный список</button>
1805</div>
1806<div style="margin-bottom: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
1807 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">📎 Загрузить привязки SKU → Запрос:</label>
1808 <div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
1809 <input type="file" id="sku-query-bindings-file" accept=".csv,.txt" style="flex: 1; padding: 8px; border: 2px solid #e0e0e0; border-radius: 8px;">
1810 <button class="ozon-parser-btn" id="upload-sku-query-btn" style="background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%);">Загрузить</button>
1811 </div>
1812 <details style="margin-top: 10px;">
1813 <summary style="cursor: pointer; font-size: 12px; color: #666;">Формат файла</summary>
1814 <div style="font-size: 12px; color: #666; margin-top: 8px; line-height: 1.6;">
1815 <strong>CSV (2 столбца):</strong> SKU,ключевой запрос<br>
1816 Пример:<br>
1817 320244429,гинкго билоба<br>
1818 320244430,витамин д3<br>
1819 320244431,омега 3<br><br>
1820 <strong>Или TXT:</strong> SKU - ключевой запрос<br>
1821 Пример:<br>
1822 320244429 - гинкго билоба<br><br>
1823 💡 В Excel: столбец A = SKU, столбец B = запрос. Сохраните как CSV (разделители - запятые).
1824 </div>
1825</details>
1826</div>
1827 <textarea class="ozon-parser-textarea" placeholder="Например: гинкго билоба аргинин витамин д"></textarea>
1828 <div style="margin-top: 15px; display: flex; gap: 10px; align-items: center;">
1829 <input type="text" class="ozon-parser-search" id="list-name-input" placeholder="Название списка (например: БАДы март 2024)" style="flex: 1; margin-bottom: 0;">
1830 <button class="ozon-parser-btn secondary" id="save-list-btn">Сохранить список</button>
1831 </div>
1832 <div style="margin-top: 15px;">
1833 <button class="ozon-parser-btn secondary" id="manage-costs-btn" style="width: 100%;">Управление расходами (себестоимость, комиссия, доставка)</button>
1834 </div>
1835 <div class="ozon-parser-progress" style="display: none;">
1836 <div class="ozon-parser-progress-text">Обработка запросов...</div>
1837 <div class="ozon-parser-progress-bar">
1838 <div class="ozon-parser-progress-fill" style="width: 0%"></div>
1839 </div>
1840 </div>
1841 </div>
1842 <div class="ozon-parser-modal-footer">
1843 <button class="ozon-parser-btn" id="cancel-parse-btn">Отмена</button>
1844 <button class="ozon-parser-btn" id="start-parsing-btn">Начать парсинг</button>
1845 </div>
1846 `;
1847
1848 modal.appendChild(content);
1849 document.body.appendChild(modal);
1850
1851 // Закрытие по клику на фон
1852 modal.addEventListener('click', (e) => {
1853 if (e.target === modal) {
1854 modal.remove();
1855 }
1856 });
1857
1858 // Обработчик кнопки отмены
1859 const cancelBtn = content.querySelector('#cancel-parse-btn');
1860 cancelBtn.addEventListener('click', () => {
1861 modal.remove();
1862 });
1863
1864 // Обработчик кнопки расчета скидки
1865 const calculateDiscountBtn = content.querySelector('#calculate-discount-btn');
1866 calculateDiscountBtn.addEventListener('click', async () => {
1867 calculateDiscountBtn.disabled = true;
1868 calculateDiscountBtn.textContent = 'Расчет...';
1869
1870 try {
1871 // Сохраняем флаг для автоматического расчета
1872 await GM.setValue('ozon_parser_calculate_discount', 'true');
1873
1874 // Открываем страницу в новой вкладке
1875 await GM.openInTab('https://seller.ozon.ru/app/prices/control', false);
1876
1877 // Ждем результата расчета
1878 let attempts = 0;
1879 const maxAttempts = 60; // 60 секунд максимум
1880
1881 const checkInterval = setInterval(async () => {
1882 attempts++;
1883 const calculatedDiscount = await GM.getValue('ozon_parser_calculated_discount', null);
1884 const calculateFlag = await GM.getValue('ozon_parser_calculate_discount', 'false');
1885
1886 if (calculatedDiscount !== null && calculateFlag === 'false') {
1887 // Расчет завершен
1888 clearInterval(checkInterval);
1889
1890 // Вставляем значение в поле
1891 const discountInput = content.querySelector('#ozon-discount-input');
1892 discountInput.value = parseFloat(calculatedDiscount).toFixed(1);
1893
1894 // Очищаем временное значение
1895 await GM.deleteValue('ozon_parser_calculated_discount');
1896
1897 calculateDiscountBtn.disabled = false;
1898 calculateDiscountBtn.textContent = 'Рассчитать автоматически';
1899
1900 alert(`Скидка Ozon успешно рассчитана: ${parseFloat(calculatedDiscount).toFixed(1)}%`);
1901 } else if (attempts >= maxAttempts) {
1902 // Таймаут
1903 clearInterval(checkInterval);
1904 calculateDiscountBtn.disabled = false;
1905 calculateDiscountBtn.textContent = 'Рассчитать автоматически';
1906 alert('Не удалось рассчитать скидку. Попробуйте еще раз.');
1907 }
1908 }, 1000);
1909 } catch (error) {
1910 console.error('Ozon Product Parser: Error calculating discount:', error);
1911 calculateDiscountBtn.disabled = false;
1912 calculateDiscountBtn.textContent = 'Рассчитать автоматически';
1913 alert('Ошибка при расчете скидки: ' + error.message);
1914 }
1915 });
1916
1917 // Обработчик кнопки сохранения списка
1918 const saveListBtn = content.querySelector('#save-list-btn');
1919 saveListBtn.addEventListener('click', async () => {
1920 const textarea = content.querySelector('.ozon-parser-textarea');
1921 const listNameInput = content.querySelector('#list-name-input');
1922 const queries = textarea.value.split('\n').filter(q => q.trim());
1923 const listName = listNameInput.value.trim();
1924
1925 if (queries.length === 0) {
1926 alert('Пожалуйста, введите хотя бы один запрос');
1927 return;
1928 }
1929
1930 if (!listName) {
1931 alert('Пожалуйста, введите название списка');
1932 return;
1933 }
1934
1935 // Сохраняем список
1936 const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
1937 const savedLists = JSON.parse(savedListsJson);
1938
1939 savedLists[listName] = {
1940 queries: queries,
1941 createdAt: new Date().toISOString(),
1942 updatedAt: new Date().toISOString()
1943 };
1944
1945 await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
1946
1947 alert(`Список "${listName}" успешно сохранен!`);
1948 console.log(`Ozon Product Parser: List "${listName}" saved with ${queries.length} queries`);
1949 });
1950
1951 // Обработчик кнопки загрузки списка
1952 const loadListBtn = content.querySelector('#load-list-btn');
1953 loadListBtn.addEventListener('click', async () => {
1954 await showLoadListModal(content);
1955 });
1956
1957 // Обработчик кнопки управления расходами
1958 const manageCostsBtn = content.querySelector('#manage-costs-btn');
1959 manageCostsBtn.addEventListener('click', async () => {
1960 await showManageCostsModal();
1961 });
1962 // Обработчик загрузки привязок SKU → Запрос
1963const uploadSkuQueryBtn = content.querySelector('#upload-sku-query-btn');
1964uploadSkuQueryBtn.addEventListener('click', async () => {
1965 const fileInput = content.querySelector('#sku-query-bindings-file');
1966 const file = fileInput.files[0];
1967
1968 if (!file) {
1969 alert('Пожалуйста, выберите файл');
1970 return;
1971 }
1972
1973 try {
1974 const text = await file.text();
1975 const lines = text.trim().split('\n');
1976 const bindings = {};
1977 const queries = [];
1978 let parsedCount = 0;
1979
1980 for (const line of lines) {
1981 const trimmedLine = line.trim();
1982 if (!trimmedLine || trimmedLine.toLowerCase().includes('sku')) continue; // Пропускаем заголовок
1983
1984 let sku = '';
1985 let query = '';
1986
1987 // Определяем формат: CSV (запятая) или TXT (дефис)
1988 if (trimmedLine.includes(',')) {
1989 // CSV формат: SKU,запрос
1990 const parts = trimmedLine.split(',');
1991 if (parts.length >= 2) {
1992 sku = parts[0].trim().replace(/^["']|["']$/g, ''); // Убираем кавычки
1993 query = parts.slice(1).join(',').trim().replace(/^["']|["']$/g, ''); // Остальное - запрос (может содержать запятые)
1994 }
1995 } else if (trimmedLine.includes('-')) {
1996 // TXT формат: SKU - запрос
1997 const parts = trimmedLine.split(/\s*-\s*/);
1998 if (parts.length >= 2) {
1999 sku = parts[0].trim();
2000 query = parts.slice(1).join(' - ').trim(); // На случай если в запросе тоже есть дефисы
2001 }
2002 }
2003
2004 if (sku && query) {
2005 bindings[sku] = query;
2006 if (!queries.includes(query)) {
2007 queries.push(query);
2008 }
2009 parsedCount++;
2010 }
2011 }
2012
2013 if (Object.keys(bindings).length === 0) {
2014 alert('Не удалось распознать данные. Проверьте формат:\n\nCSV: SKU,запрос\nTXT: SKU - запрос');
2015 return;
2016 }
2017
2018 // Сохраняем привязки
2019 const existingBindingsJson = await GM.getValue('ozon_parser_sku_query_bindings', '{}');
2020 const existingBindings = JSON.parse(existingBindingsJson);
2021 const mergedBindings = { ...existingBindings, ...bindings };
2022 await GM.setValue('ozon_parser_sku_query_bindings', JSON.stringify(mergedBindings));
2023
2024 // Добавляем запросы в textarea (если их там еще нет)
2025 const textarea = content.querySelector('.ozon-parser-textarea');
2026 const existingQueries = textarea.value.split('\n').filter(q => q.trim());
2027 const newQueries = queries.filter(q => !existingQueries.includes(q));
2028
2029 if (newQueries.length > 0) {
2030 const updatedQueries = [...existingQueries, ...newQueries];
2031 textarea.value = updatedQueries.join('\n');
2032 }
2033
2034 // Очищаем input
2035 fileInput.value = '';
2036
2037 // Формируем сообщение о результате
2038 const message = [
2039 `✅ Успешно загружено ${parsedCount} привязок SKU → Запрос`,
2040 `📋 Добавлено ${newQueries.length} новых запросов в список`,
2041 ``,
2042 `📎 Привязанные SKU:`,
2043 ...Object.entries(bindings).slice(0, 10).map(([sku, query]) => ` ${sku} → ${query}`),
2044 Object.keys(bindings).length > 10 ? ` ... и ещё ${Object.keys(bindings).length - 10}` : ''
2045 ].filter(Boolean).join('\n');
2046
2047 alert(message);
2048
2049 console.log('Ozon Product Parser: SKU-Query bindings loaded:', bindings);
2050 } catch (error) {
2051 console.error('Ozon Product Parser: Error loading SKU-Query bindings:', error);
2052 alert('Ошибка при загрузке файла: ' + error.message);
2053 }
2054});
2055
2056 // Обработчик кнопки парсинга
2057 const startBtn = content.querySelector('#start-parsing-btn');
2058 startBtn.addEventListener('click', async () => {
2059 const textarea = content.querySelector('.ozon-parser-textarea');
2060 const listNameInput = content.querySelector('#list-name-input');
2061 const ozonDiscountInput = content.querySelector('#ozon-discount-input');
2062 const queries = textarea.value.split('\n').filter(q => q.trim());
2063 const listName = listNameInput.value.trim() || 'Без названия';
2064 const ozonDiscount = parseFloat(ozonDiscountInput.value) || 50;
2065
2066 if (queries.length === 0) {
2067 alert('Пожалуйста, введите хотя бы один запрос');
2068 return;
2069 }
2070
2071 if (ozonDiscount < 0 || ozonDiscount > 100) {
2072 alert('Скидка Ozon должна быть от 0 до 100%');
2073 return;
2074 }
2075
2076 // Сохраняем скидку Ozon
2077 await GM.setValue('ozon_parser_discount', ozonDiscount);
2078
2079 startBtn.disabled = true;
2080 startBtn.textContent = 'Парсинг...';
2081 await startParsing(queries, listName, content);
2082 });
2083
2084 console.log('Ozon Product Parser: Parse modal shown');
2085 }
2086
2087 // Показываем модальное окно для загрузки сохраненного списка
2088 async function showLoadListModal(parentContent) {
2089 const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
2090 const savedLists = JSON.parse(savedListsJson);
2091 const listNames = Object.keys(savedLists);
2092
2093 if (listNames.length === 0) {
2094 alert('Нет сохраненных списков');
2095 return;
2096 }
2097
2098 const loadModal = document.createElement('div');
2099 loadModal.className = 'ozon-parser-modal';
2100 loadModal.style.zIndex = '10002';
2101
2102 const loadContent = document.createElement('div');
2103 loadContent.className = 'ozon-parser-modal-content';
2104 loadContent.style.maxWidth = '600px';
2105
2106 let listsHTML = '<div class="ozon-parser-modal-header">Выберите список</div><div class="ozon-parser-modal-body">';
2107
2108 listNames.forEach(listName => {
2109 const list = savedLists[listName];
2110 const date = new Date(list.createdAt).toLocaleDateString('ru-RU');
2111 const queriesCount = list.queries.length;
2112 listsHTML += `
2113 <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;"
2114 class="saved-list-item" data-list-name="${listName}">
2115 <div style="flex: 1; cursor: pointer;" class="saved-list-info">
2116 <div style="font-weight: 600; font-size: 16px; margin-bottom: 5px;">${listName}</div>
2117 <div style="font-size: 13px; color: #666;">Запросов: ${queriesCount} | Создан: ${date}</div>
2118 </div>
2119 <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>
2120 </div>
2121 `;
2122 });
2123
2124 listsHTML += '</div><div class="ozon-parser-modal-footer"><button class="ozon-parser-btn" id="close-load-modal">Отмена</button></div>';
2125
2126 loadContent.innerHTML = listsHTML;
2127 loadModal.appendChild(loadContent);
2128 document.body.appendChild(loadModal);
2129
2130 // Обработчик выбора списка (только для info блока)
2131 loadContent.querySelectorAll('.saved-list-info').forEach(info => {
2132 info.addEventListener('click', () => {
2133 const listItem = info.closest('.saved-list-item');
2134 const listName = listItem.getAttribute('data-list-name');
2135 const list = savedLists[listName];
2136
2137 // Заполняем textarea
2138 const textarea = parentContent.querySelector('.ozon-parser-textarea');
2139 const listNameInput = parentContent.querySelector('#list-name-input');
2140 textarea.value = list.queries.join('\n');
2141 listNameInput.value = listName;
2142
2143 loadModal.remove();
2144 });
2145
2146 // Hover эффект
2147 const listItem = info.closest('.saved-list-item');
2148 info.addEventListener('mouseenter', () => {
2149 listItem.style.borderColor = '#005bff';
2150 listItem.style.background = '#f8f9fa';
2151 });
2152 info.addEventListener('mouseleave', () => {
2153 listItem.style.borderColor = '#e0e0e0';
2154 listItem.style.background = 'white';
2155 });
2156 });
2157
2158 // Обработчик удаления списка
2159 loadContent.querySelectorAll('[data-delete-list]').forEach(deleteBtn => {
2160 deleteBtn.addEventListener('click', async (e) => {
2161 e.stopPropagation();
2162 const listName = deleteBtn.getAttribute('data-delete-list');
2163
2164 if (!confirm(`Вы уверены, что хотите удалить список "${listName}"?`)) {
2165 return;
2166 }
2167
2168 // Удаляем список из savedLists
2169 delete savedLists[listName];
2170 await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
2171
2172 // Также удаляем результаты парсинга для этого списка
2173 const listResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
2174 const listResults = JSON.parse(listResultsJson);
2175 delete listResults[listName];
2176 await GM.setValue('ozon_parser_list_results', JSON.stringify(listResults));
2177
2178 console.log(`Ozon Product Parser: List "${listName}" deleted from load modal`);
2179
2180 // Удаляем элемент из DOM
2181 const listItem = deleteBtn.closest('.saved-list-item');
2182 listItem.remove();
2183
2184 // Если списков не осталось, закрываем модальное окно
2185 const remainingLists = loadContent.querySelectorAll('.saved-list-item');
2186 if (remainingLists.length === 0) {
2187 alert('Все списки удалены');
2188 loadModal.remove();
2189 }
2190 });
2191 });
2192
2193 // Закрытие
2194 loadContent.querySelector('#close-load-modal').addEventListener('click', () => {
2195 loadModal.remove();
2196 });
2197
2198 loadModal.addEventListener('click', (e) => {
2199 if (e.target === loadModal) {
2200 loadModal.remove();
2201 }
2202 });
2203 }
2204
2205 // Показываем модальное окно управления расходами
2206 async function showManageCostsModal() {
2207 const costsJson = await GM.getValue('ozon_parser_costs', '{}');
2208 const costs = JSON.parse(costsJson);
2209
2210 const modal = document.createElement('div');
2211 modal.className = 'ozon-parser-modal';
2212 modal.style.zIndex = '10002';
2213
2214 const content = document.createElement('div');
2215 content.className = 'ozon-parser-modal-content';
2216 content.style.maxWidth = '800px';
2217
2218 content.innerHTML = `
2219 <div class="ozon-parser-modal-header">Управление расходами</div>
2220 <div class="ozon-parser-modal-body">
2221 <div class="ozon-parser-info">
2222 Укажите расходы для ваших товаров (SKU). Эти данные будут использоваться для расчета прибыли и оптимальной цены.
2223 </div>
2224
2225 <!-- Блок загрузки файла -->
2226 <div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
2227 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Загрузить данные из файла:</label>
2228 <div style="display: flex; gap: 10px; align-items: center; margin-bottom: 10px;">
2229 <input type="file" id="cost-file-input" accept=".csv,.json,.txt" style="flex: 1; padding: 8px; border: 2px solid #e0e0e0; border-radius: 8px;">
2230 <button class="ozon-parser-btn" id="upload-costs-btn">Загрузить</button>
2231 </div>
2232 <details style="margin-top: 10px;">
2233 <summary style="cursor: pointer; font-size: 12px; color: #666;">Формат файлов</summary>
2234 <div style="font-size: 12px; color: #666; margin-top: 8px; line-height: 1.6;">
2235 <strong>CSV/TXT:</strong> SKU,Себестоимость,Комиссия,Доставка<br>
2236 Пример: 320244429,158.4,50,90<br><br>
2237 <strong>JSON:</strong> {"SKU": {"cost": 158.4, "commission": 0.5, "delivery": 90}}<br>
2238 Примечание: Комиссия в CSV указывается в процентах (50), в JSON - как десятичная дробь (0.5)
2239 </div>
2240 </details>
2241 </div>
2242
2243 <div style="margin-bottom: 15px;">
2244 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Добавить новый товар:</label>
2245 <div style="display: grid; grid-template-columns: 2fr 1fr 1fr 1fr auto; gap: 10px; align-items: end;">
2246 <div>
2247 <label style="font-size: 12px; color: #666;">SKU</label>
2248 <input type="text" class="ozon-parser-search" id="new-sku" placeholder="320244429" style="margin-bottom: 0;">
2249 </div>
2250 <div>
2251 <label style="font-size: 12px; color: #666;">Себестоимость (₽)</label>
2252 <input type="number" class="ozon-parser-search" id="new-cost" placeholder="158.4" step="0.01" style="margin-bottom: 0;">
2253 </div>
2254 <div>
2255 <label style="font-size: 12px; color: #666;">Комиссия (%)</label>
2256 <input type="number" class="ozon-parser-search" id="new-commission" placeholder="50" step="0.1" style="margin-bottom: 0;">
2257 </div>
2258 <div>
2259 <label style="font-size: 12px; color: #666;">Доставка (₽)</label>
2260 <input type="number" class="ozon-parser-search" id="new-delivery" placeholder="90" step="0.01" style="margin-bottom: 0;">
2261 </div>
2262 <button class="ozon-parser-btn" id="add-cost-btn" style="margin-bottom: 0;">Добавить</button>
2263 </div>
2264 </div>
2265 <div id="costs-list" style="max-height: 400px; overflow-y: auto;">
2266 ${Object.keys(costs).length === 0 ? '<p style="text-align: center; color: #999;">Нет добавленных товаров</p>' : ''}
2267 </div>
2268 </div>
2269 <div class="ozon-parser-modal-footer">
2270 <button class="ozon-parser-btn" id="close-costs-modal">Закрыть</button>
2271 </div>
2272 `;
2273
2274 modal.appendChild(content);
2275 document.body.appendChild(modal);
2276
2277 // Функция для парсинга CSV
2278 function parseCSV(text) {
2279 const lines = text.trim().split('\n');
2280 const result = {};
2281
2282 for (let i = 0; i < lines.length; i++) {
2283 const line = lines[i].trim();
2284 if (!line || line.startsWith('SKU')) continue; // Пропускаем заголовок
2285
2286 const parts = line.split(',').map(p => p.trim());
2287 if (parts.length < 4) continue;
2288
2289 // Удаляем кавычки из SKU, если они есть
2290 let sku = parts[0].replace(/^["']|["']$/g, '');
2291 const cost = parseFloat(parts[1]);
2292 const commission = parseFloat(parts[2]);
2293 const delivery = parseFloat(parts[3]);
2294
2295 if (sku && !isNaN(cost) && !isNaN(commission) && !isNaN(delivery)) {
2296 result[sku] = {
2297 cost: cost,
2298 commission: commission / 100, // Конвертируем проценты в десятичную дробь
2299 delivery: delivery
2300 };
2301 }
2302 }
2303
2304 return result;
2305 }
2306
2307 // Функция для парсинга JS-объекта из txt файла
2308 function parseJSObject(text) {
2309 try {
2310 // Удаляем возможные комментарии и лишние пробелы
2311 let cleanText = text.trim();
2312
2313 // Если текст не начинается с {, добавляем открывающую скобку
2314 if (!cleanText.startsWith('{')) {
2315 cleanText = '{' + cleanText;
2316 }
2317
2318 // Если текст не заканчивается на }, добавляем закрывающую скобку
2319 if (!cleanText.endsWith('}')) {
2320 // Удаляем последнюю запятую, если она есть
2321 cleanText = cleanText.replace(/,\s*$/, '');
2322 cleanText = cleanText + '}';
2323 }
2324
2325 console.log('Ozon Product Parser: Attempting to parse JS object, length:', cleanText.length);
2326
2327 // Используем eval для парсинга JS объекта (безопасно, т.к. это локальный файл пользователя)
2328 const result = eval('(' + cleanText + ')');
2329
2330 // Валидация формата
2331 if (typeof result !== 'object' || result === null) {
2332 throw new Error('Неверный формат: ожидается объект');
2333 }
2334
2335 // Проверяем структуру данных
2336 for (const sku in result) {
2337 const item = result[sku];
2338 if (typeof item !== 'object' || item === null) {
2339 throw new Error(`Неверный формат данных для SKU ${sku}. Ожидается объект с полями cost, commission, delivery`);
2340 }
2341 if (typeof item.cost !== 'number' || typeof item.commission !== 'number' || typeof item.delivery !== 'number') {
2342 throw new Error(`Неверный формат данных для SKU ${sku}. Ожидается: { cost: число, commission: число (0-1), delivery: число }. Получено: ${JSON.stringify(item)}`);
2343 }
2344 }
2345
2346 console.log('Ozon Product Parser: Successfully parsed JS object with', Object.keys(result).length, 'items');
2347 return result;
2348 } catch (error) {
2349 console.error('Ozon Product Parser: JS object parse error:', error);
2350 throw new Error(`Ошибка парсинга JS-объекта: ${error.message}`);
2351 }
2352 }
2353
2354 // Обработчик загрузки файла
2355 const uploadBtn = content.querySelector('#upload-costs-btn');
2356 uploadBtn.addEventListener('click', async () => {
2357 const fileInput = content.querySelector('#cost-file-input');
2358 const file = fileInput.files[0];
2359
2360 if (!file) {
2361 alert('Пожалуйста, выберите файл');
2362 return;
2363 }
2364
2365 try {
2366 const text = await file.text();
2367 let newCosts = {};
2368
2369 console.log('Ozon Product Parser: Processing file:', file.name, 'Size:', file.size, 'bytes');
2370
2371 if (file.name.endsWith('.json')) {
2372 // Парсим JSON
2373 try {
2374 newCosts = JSON.parse(text);
2375 console.log('Ozon Product Parser: Parsed JSON successfully');
2376 } catch (jsonError) {
2377 console.error('Ozon Product Parser: JSON parse error:', jsonError);
2378 throw new Error(`Ошибка парсинга JSON: ${jsonError.message}. Проверьте, что файл содержит корректный JSON формат.`);
2379 }
2380
2381 // Валидация JSON формата
2382 for (const sku in newCosts) {
2383 const item = newCosts[sku];
2384 if (typeof item !== 'object' || item === null) {
2385 throw new Error(`Неверный формат данных для SKU ${sku}. Ожидается объект с полями cost, commission, delivery`);
2386 }
2387 if (typeof item.cost !== 'number' || typeof item.commission !== 'number' || typeof item.delivery !== 'number') {
2388 throw new Error(`Неверный формат данных для SKU ${sku}. Ожидается: {"cost": число, "commission": число, "delivery": число}. Получено: ${JSON.stringify(item)}`);
2389 }
2390 }
2391 } else if (file.name.endsWith('.txt')) {
2392 // Пытаемся определить формат: JS-объект или CSV
2393 const trimmedText = text.trim();
2394 console.log('Ozon Product Parser: Processing TXT file, first 100 chars:', trimmedText.substring(0, 100));
2395
2396 if (trimmedText.startsWith('{') || trimmedText.includes('{')) {
2397 // JS-объект формат
2398 console.log('Ozon Product Parser: Detected JS object format');
2399 newCosts = parseJSObject(text);
2400 } else {
2401 // CSV формат
2402 console.log('Ozon Product Parser: Detected CSV format');
2403 newCosts = parseCSV(text);
2404 }
2405 } else {
2406 // CSV формат для других расширений
2407 console.log('Ozon Product Parser: Processing as CSV format');
2408 newCosts = parseCSV(text);
2409 }
2410
2411 console.log('Ozon Product Parser: Extracted', Object.keys(newCosts).length, 'items from file');
2412
2413 if (Object.keys(newCosts).length === 0) {
2414 alert('Не удалось извлечь данные из файла. Проверьте формат.\n\nОжидаемые форматы:\n\nCSV: SKU,Себестоимость,Комиссия,Доставка\nПример: 320244429,158.4,50,90\n\nJSON: {"SKU": {"cost": 158.4, "commission": 0.5, "delivery": 90}}');
2415 return;
2416 }
2417
2418 // Объединяем с существующими данными
2419 const mergedCosts = { ...costs, ...newCosts };
2420 await GM.setValue('ozon_parser_costs', JSON.stringify(mergedCosts));
2421
2422 // Обновляем costs переменную
2423 Object.assign(costs, newCosts);
2424
2425 alert(`Успешно загружено ${Object.keys(newCosts).length} товаров`);
2426 console.log('Ozon Product Parser: Successfully saved costs data');
2427 displayCostsList();
2428
2429 // Очищаем input
2430 fileInput.value = '';
2431 } catch (error) {
2432 console.error('Ozon Product Parser: Error uploading costs file:', error);
2433 console.error('Ozon Product Parser: Error stack:', error.stack);
2434 console.error('Ozon Product Parser: Error name:', error.name);
2435 console.error('Ozon Product Parser: Error message:', error.message);
2436 alert('Ошибка при загрузке файла:\n\n' + error.message + '\n\nПроверьте формат файла и попробуйте снова.');
2437 }
2438 });
2439
2440 // Функция для отображения списка расходов
2441 function displayCostsList() {
2442 const costsList = content.querySelector('#costs-list');
2443
2444 if (Object.keys(costs).length === 0) {
2445 costsList.innerHTML = '<p style="text-align: center; color: #999;">Нет добавленных товаров</p>';
2446 return;
2447 }
2448
2449 let html = '<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">';
2450 html += '<div style="font-size: 14px; font-weight: 600;">Всего товаров: ' + Object.keys(costs).length + '</div>';
2451 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>';
2452 html += '</div>';
2453 html += '<table class="ozon-parser-results-table"><thead><tr><th>SKU</th><th>Себестоимость</th><th>Комиссия</th><th>Доставка</th><th>Действия</th></tr></thead><tbody>';
2454
2455 Object.keys(costs).forEach(sku => {
2456 const cost = costs[sku];
2457 html += `
2458 <tr>
2459 <td><a href="https://www.ozon.ru/product/${sku}" target="_blank" class="ozon-parser-sku-link">${sku}</a></td>
2460 <td>${cost.cost.toFixed(2)} ₽</td>
2461 <td>${(cost.commission * 100).toFixed(2)}%</td>
2462 <td>${cost.delivery.toFixed(2)} ₽</td>
2463 <td>
2464 <button class="ozon-parser-btn" data-edit-sku="${sku}" style="padding: 6px 12px; font-size: 12px; margin-right: 5px;">Изменить</button>
2465 <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>
2466 </td>
2467 </tr>
2468 `;
2469 });
2470
2471 html += '</tbody></table>';
2472 costsList.innerHTML = html;
2473
2474 // Обработчик для кнопки удаления всех расходов
2475 const deleteAllBtn = costsList.querySelector('#delete-all-costs-btn');
2476 if (deleteAllBtn) {
2477 deleteAllBtn.addEventListener('click', async () => {
2478 if (confirm('Вы уверены, что хотите удалить ВСЕ данные о расходах? Это действие нельзя отменить.')) {
2479 // Очищаем все данные
2480 Object.keys(costs).forEach(key => delete costs[key]);
2481 await GM.setValue('ozon_parser_costs', JSON.stringify({}));
2482 displayCostsList();
2483 alert('Все данные о расходах удалены');
2484 }
2485 });
2486 }
2487
2488 // Обработчики для кнопок удаления
2489 costsList.querySelectorAll('[data-delete-sku]').forEach(btn => {
2490 btn.addEventListener('click', async () => {
2491 const sku = btn.getAttribute('data-delete-sku');
2492 if (confirm(`Удалить данные о расходах для SKU ${sku}?`)) {
2493 delete costs[sku];
2494 await GM.setValue('ozon_parser_costs', JSON.stringify(costs));
2495 displayCostsList();
2496 }
2497 });
2498 });
2499
2500 // Обработчики для кнопок изменения
2501 costsList.querySelectorAll('[data-edit-sku]').forEach(btn => {
2502 btn.addEventListener('click', () => {
2503 const sku = btn.getAttribute('data-edit-sku');
2504 const cost = costs[sku];
2505
2506 content.querySelector('#new-sku').value = sku;
2507 content.querySelector('#new-cost').value = cost.cost;
2508 content.querySelector('#new-commission').value = cost.commission * 100;
2509 content.querySelector('#new-delivery').value = cost.delivery;
2510
2511 content.querySelector('#new-sku').scrollIntoView({ behavior: 'smooth', block: 'center' });
2512 });
2513 });
2514 }
2515
2516 displayCostsList();
2517
2518 // Обработчик добавления нового товара
2519 const addBtn = content.querySelector('#add-cost-btn');
2520 addBtn.addEventListener('click', async () => {
2521 const sku = content.querySelector('#new-sku').value.trim();
2522 const cost = parseFloat(content.querySelector('#new-cost').value);
2523 const commission = parseFloat(content.querySelector('#new-commission').value);
2524 const delivery = parseFloat(content.querySelector('#new-delivery').value);
2525
2526 if (!sku) {
2527 alert('Пожалуйста, введите SKU');
2528 return;
2529 }
2530
2531 if (isNaN(cost) || cost < 0) {
2532 alert('Пожалуйста, введите корректную себестоимость');
2533 return;
2534 }
2535
2536 if (isNaN(commission) || commission < 0 || commission > 100) {
2537 alert('Пожалуйста, введите корректную комиссию (0-100%)');
2538 return;
2539 }
2540
2541 if (isNaN(delivery) || delivery < 0) {
2542 alert('Пожалуйста, введите корректную стоимость доставки');
2543 return;
2544 }
2545
2546 costs[sku] = {
2547 cost: cost,
2548 commission: commission / 100, // Сохраняем как десятичную дробь
2549 delivery: delivery
2550 };
2551
2552 await GM.setValue('ozon_parser_costs', JSON.stringify(costs));
2553
2554 // Очищаем поля
2555 content.querySelector('#new-sku').value = '';
2556 content.querySelector('#new-cost').value = '';
2557 content.querySelector('#new-commission').value = '';
2558 content.querySelector('#new-delivery').value = '';
2559
2560 displayCostsList();
2561 });
2562
2563 // Закрытие
2564 content.querySelector('#close-costs-modal').addEventListener('click', () => {
2565 modal.remove();
2566 });
2567
2568 modal.addEventListener('click', (e) => {
2569 if (e.target === modal) {
2570 modal.remove();
2571 }
2572 });
2573 }
2574
2575 // Извлекаем скидку со страницы управления ценами
2576 async function extractDiscountFromPage() {
2577 console.log('Ozon Product Parser: Extracting discount from prices page');
2578
2579 // Ждем загрузки таблицы с ценами
2580 await waitForPricesTable();
2581
2582 // Прокручиваем страницу вниз для загрузки цен (ленивая загрузка)
2583 console.log('Ozon Product Parser: Scrolling to load prices');
2584 for (let i = 0; i < 3; i++) {
2585 window.scrollBy(0, 500);
2586 await new Promise(resolve => setTimeout(resolve, 2000));
2587 }
2588
2589 try {
2590 // Ищем цены по правильным классам
2591 // Цена продажи (с картой Ozon): index_priceByOzonCardCurrency_3DLKf
2592 // Базовая цена (цена поручения): index_priceAmount_3dfpL
2593
2594 const salePriceElements = document.querySelectorAll('.index_priceByOzonCardCurrency_3DLKf');
2595 const basePriceElements = document.querySelectorAll('.index_priceAmount_3dfpL');
2596
2597 console.log(`Ozon Product Parser: Found ${salePriceElements.length} sale prices and ${basePriceElements.length} base prices`);
2598
2599 if (salePriceElements.length === 0 || basePriceElements.length === 0) {
2600 console.error('Ozon Product Parser: Could not find price elements on the page');
2601 alert('Не удалось найти цены на странице. Убедитесь, что у вас есть товары с ценами.');
2602 await GM.setValue('ozon_parser_calculate_discount', 'false');
2603 return;
2604 }
2605
2606 // Собираем пары цен (базовая и продажная)
2607 let validDiscounts = [];
2608
2609 // Ищем строки таблицы с обеими ценами
2610 const rows = document.querySelectorAll('tr');
2611 for (const row of rows) {
2612 const salePrice = row.querySelector('.index_priceByOzonCardCurrency_3DLKf');
2613 const basePrice = row.querySelector('.index_priceAmount_3dfpL');
2614
2615 if (salePrice && basePrice) {
2616 const salePriceText = salePrice.textContent.trim();
2617 const basePriceText = basePrice.textContent.trim();
2618
2619 // Парсим цены (убираем пробелы и символ рубля)
2620 const salePriceValue = parseFloat(salePriceText.replace(/\s/g, '').replace('₽', '').replace(',', '.'));
2621 const basePriceValue = parseFloat(basePriceText.replace(/\s/g, '').replace('₽', '').replace(',', '.'));
2622
2623 if (!isNaN(salePriceValue) && !isNaN(basePriceValue) && basePriceValue > 0 && salePriceValue > 0) {
2624 // Рассчитываем скидку: (базовая цена - цена продажи) / базовая цена * 100
2625 const discountAmount = basePriceValue - salePriceValue;
2626 const discountPercent = (discountAmount / basePriceValue) * 100;
2627
2628 if (discountPercent > 0 && discountPercent < 100) {
2629 validDiscounts.push({
2630 basePrice: basePriceValue,
2631 salePrice: salePriceValue,
2632 discount: discountPercent
2633 });
2634
2635 console.log(`Ozon Product Parser: Base price: ${basePriceValue}₽, Sale price: ${salePriceValue}₽, Discount: ${discountPercent.toFixed(1)}%`);
2636 }
2637 }
2638 }
2639 }
2640
2641 if (validDiscounts.length === 0) {
2642 console.error('Ozon Product Parser: No valid price pairs found');
2643 alert('Не удалось найти валидные пары цен на странице.');
2644 await GM.setValue('ozon_parser_calculate_discount', 'false');
2645 return;
2646 }
2647
2648 // Рассчитываем среднюю скидку
2649 const avgDiscount = validDiscounts.reduce((sum, item) => sum + item.discount, 0) / validDiscounts.length;
2650
2651 console.log(`Ozon Product Parser: Calculated average discount from ${validDiscounts.length} products: ${avgDiscount.toFixed(1)}%`);
2652
2653 // Сохраняем рассчитанную скидку для передачи в основное окно
2654 await GM.setValue('ozon_parser_calculated_discount', avgDiscount);
2655
2656 // Сбрасываем флаг расчета
2657 await GM.setValue('ozon_parser_calculate_discount', 'false');
2658
2659 // Закрываем текущую вкладку
2660 window.close();
2661 } catch (error) {
2662 console.error('Ozon Product Parser: Error extracting discount:', error);
2663 alert('Ошибка при извлечении данных о скидке: ' + error.message);
2664
2665 // Сбрасываем флаг расчета
2666 await GM.setValue('ozon_parser_calculate_discount', 'false');
2667 }
2668 }
2669
2670 // Ждем появления таблицы с ценами
2671 function waitForPricesTable() {
2672 return new Promise((resolve) => {
2673 const checkTable = () => {
2674 const rows = document.querySelectorAll('tr');
2675 if (rows.length > 0) {
2676 console.log('Ozon Product Parser: Prices table found');
2677 // Дополнительная задержка для полной загрузки данных
2678 setTimeout(resolve, 3000);
2679 } else {
2680 setTimeout(checkTable, 1000);
2681 }
2682 };
2683 checkTable();
2684 });
2685 }
2686
2687 // Прокручиваем страницу для подгрузки товаров
2688 async function scrollToLoadProducts(targetCount = 20) {
2689 console.log(`Ozon Product Parser: Scrolling to load ${targetCount} products`);
2690
2691 const table = document.querySelector('#mpstat-ozone-search-result table');
2692 if (!table) {
2693 console.error('Ozon Product Parser: Table not found for scrolling');
2694 return;
2695 }
2696
2697 let previousRowCount = 0;
2698 let attempts = 0;
2699 const maxAttempts = 10;
2700
2701 while (attempts < maxAttempts) {
2702 const rows = table.querySelectorAll('tbody tr');
2703 const currentRowCount = rows.length;
2704
2705 console.log(`Ozon Product Parser: Current row count: ${currentRowCount}, target: ${targetCount}`);
2706
2707 if (currentRowCount >= targetCount) {
2708 console.log(`Ozon Product Parser: Loaded ${currentRowCount} products`);
2709 break;
2710 }
2711
2712 // Если количество строк не изменилось, значит больше товаров нет
2713 if (currentRowCount === previousRowCount && attempts > 2) {
2714 console.log(`Ozon Product Parser: No more products to load (${currentRowCount} total)`);
2715 break;
2716 }
2717
2718 previousRowCount = currentRowCount;
2719
2720 // Прокручиваем к последней строке таблицы
2721 const lastRow = rows[rows.length - 1];
2722 if (lastRow) {
2723 lastRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
2724 }
2725
2726 // Также прокручиваем окно вниз
2727 window.scrollBy(0, 500);
2728
2729 // Ждем подгрузки новых товаров
2730 await new Promise(resolve => setTimeout(resolve, 2000));
2731 attempts++;
2732 }
2733
2734 console.log(`Ozon Product Parser: Scrolling completed after ${attempts} attempts`);
2735 }
2736
2737 // Начинаем парсинг
2738 async function startParsing(queries, listName, modalContent) {
2739 const progressDiv = modalContent.querySelector('.ozon-parser-progress');
2740 progressDiv.style.display = 'block';
2741
2742 // Сохраняем список запросов и название списка
2743 await GM.setValue('ozon_parser_queries', JSON.stringify(queries));
2744 await GM.setValue('ozon_parser_current_list_name', listName);
2745 await GM.setValue('ozon_parser_current_index', 0);
2746 await GM.setValue('ozon_parser_active', 'true');
2747 console.log(`Ozon Product Parser: Starting parsing process for list "${listName}"`);
2748
2749 // Переходим к первому запросу
2750 const firstQuery = queries[0].trim();
2751 const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(firstQuery)}&from_global=true`;
2752 window.location.href = searchUrl;
2753 }
2754
2755 // Продолжаем парсинг после загрузки страницы
2756 async function continueParsingIfActive() {
2757 const isActive = await GM.getValue('ozon_parser_active', 'false');
2758 if (isActive !== 'true') {
2759 return;
2760 }
2761
2762 console.log('Ozon Product Parser: Continuing parsing process');
2763
2764 // Ждем появления таблицы
2765 await waitForTable();
2766
2767 // Получаем текущее состояние
2768 const queriesJson = await GM.getValue('ozon_parser_queries', '[]');
2769 const queries = JSON.parse(queriesJson);
2770 const currentIndex = await GM.getValue('ozon_parser_current_index', 0);
2771 const currentListName = await GM.getValue('ozon_parser_current_list_name', 'Без названия');
2772
2773 // Получаем результаты для текущего списка
2774 const allListResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
2775 const allListResults = JSON.parse(allListResultsJson);
2776
2777 if (!allListResults[currentListName]) {
2778 allListResults[currentListName] = {
2779 queries: {},
2780 createdAt: new Date().toISOString(),
2781 updatedAt: new Date().toISOString()
2782 };
2783 }
2784
2785 if (currentIndex >= queries.length) {
2786 // Парсинг завершен
2787 await GM.setValue('ozon_parser_active', 'false');
2788
2789 // Обновляем дату последнего обновления списка
2790 allListResults[currentListName].updatedAt = new Date().toISOString();
2791 await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
2792
2793 console.log('Ozon Product Parser: Parsing completed');
2794 return;
2795 }
2796
2797 const currentQuery = queries[currentIndex].trim();
2798 console.log(`Ozon Product Parser: Processing query ${currentIndex + 1}/${queries.length}: "${currentQuery}"`);
2799
2800 // Парсим данные текущей страницы
2801 const products = await parseProducts();
2802 allListResults[currentListName].queries[currentQuery] = products;
2803
2804 // Анализируем высокие цены
2805 await analyzeHighPrices(currentQuery, products);
2806
2807 // Сохраняем результаты
2808 await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
2809 console.log(`Ozon Product Parser: Parsed ${products.length} products for "${currentQuery}" in list "${currentListName}"`);
2810
2811 // Переходим к следующему запросу
2812 const nextIndex = currentIndex + 1;
2813 await GM.setValue('ozon_parser_current_index', nextIndex);
2814
2815 if (nextIndex < queries.length) {
2816 // Есть еще запросы - переходим к следующему
2817 const nextQuery = queries[nextIndex].trim();
2818 const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(nextQuery)}&from_global=true`;
2819 setTimeout(() => {
2820 window.location.href = searchUrl;
2821 }, 2000); // Небольшая задержка между запросами
2822 } else {
2823 // Все запросы обработаны
2824 await GM.setValue('ozon_parser_active', 'false');
2825
2826 // Обновляем дату последнего обновления списка
2827 allListResults[currentListName].updatedAt = new Date().toISOString();
2828 await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
2829
2830 console.log('Ozon Product Parser: All queries processed');
2831
2832 // Показываем результаты
2833 setTimeout(() => {
2834 showResultsModal();
2835 }, 1000);
2836 }
2837 }
2838
2839 // Продолжаем расчет скидки после загрузки страницы
2840 async function continueDiscountCalculationIfActive() {
2841 const shouldCalculate = await GM.getValue('ozon_parser_calculate_discount', 'false');
2842 if (shouldCalculate === 'true') {
2843 await GM.setValue('ozon_parser_calculate_discount', 'false');
2844 console.log('Ozon Product Parser: Continuing discount calculation');
2845 await extractDiscountFromPage();
2846 }
2847 }
2848
2849 // Ждем появления таблицы
2850 function waitForTable() {
2851 return new Promise((resolve) => {
2852 const checkTable = () => {
2853 const table = document.querySelector('#mpstat-ozone-search-result table tbody');
2854 if (table && table.querySelectorAll('tr').length > 0) {
2855 console.log('Ozon Product Parser: Table found');
2856 // Дополнительная задержка для полной загрузки данных
2857 setTimeout(resolve, 5000);
2858 } else {
2859 setTimeout(checkTable, 1000);
2860 }
2861 };
2862 checkTable();
2863 });
2864 }
2865
2866 // Прокручиваем страницу для подгрузки товаров
2867 async function scrollToLoadProducts(targetCount = 20) {
2868 console.log(`Ozon Product Parser: Scrolling to load ${targetCount} products`);
2869
2870 const table = document.querySelector('#mpstat-ozone-search-result table');
2871 if (!table) {
2872 console.error('Ozon Product Parser: Table not found for scrolling');
2873 return;
2874 }
2875
2876 let previousRowCount = 0;
2877 let attempts = 0;
2878 const maxAttempts = 10;
2879
2880 while (attempts < maxAttempts) {
2881 const rows = table.querySelectorAll('tbody tr');
2882 const currentRowCount = rows.length;
2883
2884 console.log(`Ozon Product Parser: Current row count: ${currentRowCount}, target: ${targetCount}`);
2885
2886 if (currentRowCount >= targetCount) {
2887 console.log(`Ozon Product Parser: Loaded ${currentRowCount} products`);
2888 break;
2889 }
2890
2891 // Если количество строк не изменилось, значит больше товаров нет
2892 if (currentRowCount === previousRowCount && attempts > 2) {
2893 console.log(`Ozon Product Parser: No more products to load (${currentRowCount} total)`);
2894 break;
2895 }
2896
2897 previousRowCount = currentRowCount;
2898
2899 // Прокручиваем к последней строке таблицы
2900 const lastRow = rows[rows.length - 1];
2901 if (lastRow) {
2902 lastRow.scrollIntoView({ behavior: 'smooth', block: 'end' });
2903 }
2904
2905 // Также прокручиваем окно вниз
2906 window.scrollBy(0, 500);
2907
2908 // Ждем подгрузки новых товаров
2909 await new Promise(resolve => setTimeout(resolve, 2000));
2910 attempts++;
2911 }
2912
2913 console.log(`Ozon Product Parser: Scrolling completed after ${attempts} attempts`);
2914 }
2915
2916 // Парсим товары из таблицы
2917 async function parseProducts() {
2918 const table = document.querySelector('#mpstat-ozone-search-result table');
2919 if (!table) {
2920 console.error('Ozon Product Parser: Table not found');
2921 return [];
2922 }
2923
2924 // Прокручиваем страницу для подгрузки товаров
2925 await scrollToLoadProducts(20);
2926
2927 const rows = table.querySelectorAll('tbody tr');
2928 const products = [];
2929 const maxProducts = Math.min(20, rows.length);
2930
2931 // Получаем названия товаров из карточек на странице
2932 const productLinks = document.querySelectorAll('a[href*="/product/"]');
2933 const productNames = new Map();
2934
2935 console.log(`Ozon Product Parser: Found ${productLinks.length} product links`);
2936
2937 productLinks.forEach(link => {
2938 const href = link.getAttribute('href');
2939 const skuMatch = href.match(/\/product\/[^/]+-(\d+)/);
2940 if (skuMatch) {
2941 const sku = skuMatch[1];
2942 // Ищем название в родительском элементе
2943 const parent = link.closest('.tile-root') || link.closest('[data-index]');
2944 if (parent) {
2945 const nameElement = parent.querySelector('.tsBody500Medium');
2946 if (nameElement && nameElement.textContent.trim().length > 10) {
2947 productNames.set(sku, nameElement.textContent.trim());
2948 }
2949 }
2950 }
2951 });
2952
2953 console.log(`Ozon Product Parser: Extracted ${productNames.size} product names`);
2954
2955 // Функция для извлечения количества единиц из названия
2956 function extractQuantity(name) {
2957 if (!name) return null;
2958
2959 // Ищем паттерны: "120 капсул", "60 таблеток", "180шт", "90 шт"
2960 const patterns = [
2961 /(\d+)\s*(?:капсул|капс|caps)/i,
2962 /(\d+)\s*(?:таблеток|табл|tablets|tabs)/i,
2963 /(\d+)\s*(?:штук|шт|pcs|pieces)/i,
2964 /(\d+)\s*(?:порций|servings)/i
2965 ];
2966
2967 for (const pattern of patterns) {
2968 const match = name.match(pattern);
2969 if (match) {
2970 const quantity = parseInt(match[1]);
2971 if (quantity > 0 && quantity <= 1000) { // Разумные пределы
2972 return quantity;
2973 }
2974 }
2975 }
2976
2977 return null;
2978 }
2979
2980 for (let i = 0; i < maxProducts; i++) {
2981 const row = rows[i];
2982 const cells = row.querySelectorAll('td');
2983 if (cells.length < 8) continue;
2984
2985 const position = cells[0]?.textContent.trim() || '';
2986 const sku = cells[2]?.textContent.trim() || '';
2987 const brand = cells[3]?.textContent.trim() || '';
2988 const priceText = cells[4]?.textContent.trim() || '';
2989 const revenueText = cells[6]?.textContent.trim() || '';
2990 const ordersText = cells[7]?.textContent.trim() || '';
2991
2992 // Получаем название товара из карточки
2993 const name = productNames.get(sku) || '';
2994
2995 // Извлекаем количество единиц
2996 const quantity = extractQuantity(name);
2997
2998 // Проверяем, есть ли товар от GLS или Skinphoria
2999 const isTargetBrand = brand.includes('GLS Pharmaceuticals') || brand.includes('Skinphoria');
3000
3001 // Парсим числовые значения
3002 const price = parseFloat(priceText.replace(/[^\d]/g, '')) || 0;
3003 const revenue = parseFloat(revenueText.replace(/[^\d]/g, '')) || 0;
3004 const orders = parseInt(ordersText.replace(/[^\d]/g, '')) || 0;
3005
3006 // Рассчитываем цену за единицу
3007 const pricePerUnit = quantity && price > 0 ? price / quantity : null;
3008
3009 products.push({
3010 position: parseInt(position) || (i + 1),
3011 sku,
3012 name,
3013 brand,
3014 price,
3015 revenue,
3016 orders,
3017 isTargetBrand,
3018 quantity,
3019 pricePerUnit
3020 });
3021 }
3022
3023 // Сортируем по убыванию выручки
3024 products.sort((a, b) => b.revenue - a.revenue);
3025 console.log(`Ozon Product Parser: Parsed ${products.length} products, ${products.filter(p => p.isTargetBrand).length} from target brands`);
3026 return products;
3027 }
3028
3029 // Анализируем высокие цены после парсинга
3030 async function analyzeHighPrices(query, products) {
3031 console.log(`Ozon Product Parser: Analyzing high prices for query "${query}"`);
3032
3033 // Получаем топ-5 конкурентов по выручке
3034 const competitors = products.filter(p => !p.isTargetBrand && p.price > 0);
3035 const top5Competitors = competitors.slice(0, 5);
3036
3037 if (top5Competitors.length === 0) {
3038 console.log('Ozon Product Parser: No competitors found for high price analysis');
3039 return;
3040 }
3041
3042 // Находим минимальную цену среди топ-5 конкурентов
3043 const minCompetitorPrice = Math.min(...top5Competitors.map(p => p.price));
3044 console.log(`Ozon Product Parser: Min competitor price in top-5: ${minCompetitorPrice}₽`);
3045
3046 // Проверяем наши товары
3047 const ourProducts = products.filter(p => p.isTargetBrand);
3048 const highPriceProducts = [];
3049
3050 for (const product of ourProducts) {
3051 if (product.price > 0) {
3052 const priceDiff = ((product.price - minCompetitorPrice) / minCompetitorPrice) * 100;
3053
3054 if (priceDiff > 10) {
3055 console.log(`Ozon Product Parser: High price detected - SKU ${product.sku}: ${product.price}₽ vs ${minCompetitorPrice}₽ (+${priceDiff.toFixed(1)}%)`);
3056
3057 highPriceProducts.push({
3058 sku: product.sku,
3059 name: product.name,
3060 ourPrice: product.price,
3061 competitorPrice: minCompetitorPrice,
3062 query: query
3063 });
3064 }
3065 }
3066 }
3067
3068 if (highPriceProducts.length > 0) {
3069 // Добавляем в общий список высоких цен
3070 const existingDataJson = await GM.getValue('ozon_parser_high_price', '[]');
3071 const existingData = JSON.parse(existingDataJson);
3072
3073 // Удаляем дубликаты по SKU + query
3074 const existingKeys = new Set(existingData.map(item => `${item.sku}_${item.query}`));
3075 const newProducts = highPriceProducts.filter(item => !existingKeys.has(`${item.sku}_${item.query}`));
3076
3077 if (newProducts.length > 0) {
3078 const updatedData = [...existingData, ...newProducts];
3079 await GM.setValue('ozon_parser_high_price', JSON.stringify(updatedData));
3080 console.log(`Ozon Product Parser: Added ${newProducts.length} products to high price list`);
3081
3082 // Обновляем счетчик
3083 await updateHighPriceCounter();
3084 }
3085 }
3086 }
3087
3088 // Анализируем данные для запроса
3089 async function analyzeProducts(products) {
3090 console.log('Ozon Product Parser: Starting analysis for', products.length, 'products');
3091
3092 if (!products || products.length === 0) {
3093 console.error('Ozon Product Parser: No products to analyze');
3094 return null;
3095 }
3096
3097 try {
3098 // Получаем данные о расходах и скидке Ozon
3099 const costsJson = await GM.getValue('ozon_parser_costs', '{}');
3100 const costs = JSON.parse(costsJson);
3101 const ozonDiscount = await GM.getValue('ozon_parser_discount', 50);
3102 console.log(`Ozon Product Parser: Loaded costs for ${Object.keys(costs).length} SKUs, Ozon discount: ${ozonDiscount}%`);
3103
3104 // Общая выручка и заказы
3105 const totalRevenue = products.reduce((sum, p) => sum + p.revenue, 0);
3106 const totalOrders = products.reduce((sum, p) => sum + p.orders, 0);
3107
3108 // Топ-5 товаров по выручке
3109 const topProducts = products.slice(0, 5).map(p => ({
3110 name: p.name,
3111 brand: p.brand,
3112 price: p.price,
3113 revenue: p.revenue,
3114 orders: p.orders,
3115 position: p.position,
3116 revenueShare: ((p.revenue / totalRevenue) * 100).toFixed(1)
3117 }));
3118
3119 // Ценовые сегменты
3120 const prices = products.map(p => p.price).filter(p => p > 0);
3121 const minPrice = Math.min(...prices);
3122 const maxPrice = Math.max(...prices);
3123 const priceRange = maxPrice - minPrice;
3124 const segmentSize = priceRange / 4;
3125
3126 const priceSegments = [
3127 { min: minPrice, max: minPrice + segmentSize, name: 'Низкий' },
3128 { min: minPrice + segmentSize, max: minPrice + segmentSize * 2, name: 'Средний-' },
3129 { min: minPrice + segmentSize * 2, max: minPrice + segmentSize * 3, name: 'Средний+' },
3130 { min: minPrice + segmentSize * 3, max: maxPrice, name: 'Высокий' }
3131 ];
3132
3133 const segments = priceSegments.map(segment => {
3134 const segmentProducts = products.filter(p =>
3135 p.price >= segment.min && p.price <= segment.max
3136 );
3137 const segmentRevenue = segmentProducts.reduce((sum, p) => sum + p.revenue, 0);
3138 const segmentOrders = segmentProducts.reduce((sum, p) => sum + p.orders, 0);
3139
3140 return {
3141 name: segment.name,
3142 priceRange: `${Math.round(segment.min)} - ${Math.round(segment.max)} ₽`,
3143 count: segmentProducts.length,
3144 revenue: segmentRevenue,
3145 orders: segmentOrders,
3146 revenueShare: ((segmentRevenue / totalRevenue) * 100).toFixed(1),
3147 avgPrice: segmentProducts.length > 0
3148 ? Math.round(segmentProducts.reduce((sum, p) => sum + p.price, 0) / segmentProducts.length)
3149 : 0
3150 };
3151 }).filter(s => s.count > 0);
3152
3153 // Расчет эластичности запроса
3154 const competitors = products.filter(p => !p.isTargetBrand);
3155 let elasticity = null;
3156 let elasticityInterpretation = '';
3157
3158 if (competitors.length >= 5) {
3159 const logPrices = competitors.map(p => Math.log(p.price + 1));
3160 const logOrders = competitors.map(p => Math.log(p.orders + 1));
3161
3162 function simpleRegression(x, y) {
3163 const n = x.length;
3164 const sumX = x.reduce((a, b) => a + b, 0);
3165 const sumY = y.reduce((a, b) => a + b, 0);
3166 const sumXY = x.reduce((a, b, i) => a + b * y[i], 0);
3167 const sumX2 = x.reduce((a, b) => a + b * b, 0);
3168 const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
3169 return slope;
3170 }
3171
3172 const rawElasticity = simpleRegression(logPrices, logOrders);
3173 const priorElasticity = -1.2;
3174 const priorWeight = 0.2;
3175 elasticity = rawElasticity * (1 - priorWeight) + priorElasticity * priorWeight;
3176
3177 console.log(`Ozon Product Parser: Raw elasticity: ${rawElasticity.toFixed(2)}, Bayesian adjusted: ${elasticity.toFixed(2)}`);
3178
3179 if (elasticity < -1.5) {
3180 elasticityInterpretation = 'Высокая эластичность - спрос очень чувствителен к цене. Снижение цены сильно увеличит продажи.';
3181 } else if (elasticity < -0.8) {
3182 elasticityInterpretation = 'Средняя эластичность - спрос умеренно реагирует на изменение цены.';
3183 } else if (elasticity < 0) {
3184 elasticityInterpretation = 'Низкая эластичность - спрос слабо зависит от цены. Можно повышать цену.';
3185 } else {
3186 elasticityInterpretation = 'Аномальная эластичность - возможно недостаточно данных для анализа.';
3187 }
3188 }
3189
3190 // Функция для расчета прогноза
3191 function calculateForecast(newPrice, currentPrice, currentOrders, currentRevenue, productCosts) {
3192 const priceChange = (newPrice - currentPrice) / currentPrice;
3193 const usedElasticity = elasticity !== null ? elasticity : -1.2;
3194 const ordersChange = usedElasticity * priceChange;
3195
3196 const forecastOrders = Math.round(currentOrders * (1 + ordersChange));
3197 const forecastRevenue = Math.round(forecastOrders * newPrice);
3198
3199 let forecastProfit = null;
3200 let currentProfit = null;
3201 let profitChange = null;
3202
3203 if (productCosts) {
3204 const basePrice = newPrice / (1 - ozonDiscount / 100);
3205 const sellerRevenue = basePrice * (1 - productCosts.commission) * forecastOrders;
3206 const expenses = (productCosts.cost + productCosts.delivery) * forecastOrders;
3207 forecastProfit = sellerRevenue - expenses;
3208
3209 const currentBasePrice = currentPrice / (1 - ozonDiscount / 100);
3210 const currentSellerRevenue = currentBasePrice * (1 - productCosts.commission) * currentOrders;
3211 const currentExpenses = (productCosts.cost + productCosts.delivery) * currentOrders;
3212 currentProfit = currentSellerRevenue - currentExpenses;
3213
3214 profitChange = currentProfit > 0 ? Math.round(((forecastProfit - currentProfit) / currentProfit) * 100) : 0;
3215 }
3216
3217 return {
3218 orders: forecastOrders,
3219 ordersChange: Math.round(ordersChange * 100),
3220 revenue: forecastRevenue,
3221 revenueChange: Math.round(((forecastRevenue - currentRevenue) / currentRevenue) * 100),
3222 profit: forecastProfit,
3223 profitChange: profitChange
3224 };
3225 }
3226
3227 // Функция для расчета рекомендованной цены для конкретного товара
3228 function calculatePriceForProduct(targetProduct, allProducts) {
3229 console.log(`Calculating price for SKU ${targetProduct.sku}`);
3230
3231 const productCosts = costs[targetProduct.sku];
3232
3233 // Находим конкурентов
3234 const positionRange = 5;
3235 const nearbyProducts = allProducts.filter(p =>
3236 !p.isTargetBrand &&
3237 Math.abs(p.position - targetProduct.position) <= positionRange &&
3238 p.price > 0 && p.orders > 0
3239 );
3240
3241 const referenceProducts = nearbyProducts.length >= 3
3242 ? nearbyProducts
3243 : allProducts.filter(p => !p.isTargetBrand && p.price > 0 && p.orders > 0).slice(0, 10);
3244
3245 if (referenceProducts.length === 0) {
3246 return null;
3247 }
3248
3249 // Рассчитываем базовую оптимальную цену на основе конкурентов
3250 let weightedSum = 0;
3251 let weightSum = 0;
3252 referenceProducts.forEach(p => {
3253 const weight = p.revenue;
3254 weightedSum += p.price * weight;
3255 weightSum += weight;
3256 });
3257 const weightedPrice = weightSum > 0 ? weightedSum / weightSum : 0;
3258
3259 const productsWithConversion = referenceProducts.map(p => ({
3260 ...p,
3261 conversion: p.orders / p.price
3262 })).sort((a, b) => b.conversion - a.conversion);
3263 const topConversionPrice = productsWithConversion.length > 0
3264 ? productsWithConversion.slice(0, 3).reduce((sum, p) => sum + p.price, 0) / 3
3265 : 0;
3266
3267 const top10Prices = referenceProducts.slice(0, 10).map(p => p.price).sort((a, b) => a - b);
3268 const medianPrice = top10Prices.length > 0
3269 ? top10Prices[Math.floor(top10Prices.length / 2)]
3270 : 0;
3271
3272 const marketOptimalPrice = (weightedPrice * 0.4 + topConversionPrice * 0.3 + medianPrice * 0.3);
3273
3274 // Рассчитываем RPI
3275 const competitorPrices = allProducts.filter(p => !p.isTargetBrand && p.price > 0);
3276 let avgCompetitorPrice = 0;
3277 if (competitorPrices.length > 0) {
3278 let weightedPriceSum = 0;
3279 let weightSum = 0;
3280 competitorPrices.forEach(comp => {
3281 const weight = comp.revenue;
3282 weightedPriceSum += comp.price * weight;
3283 weightSum += weight;
3284 });
3285 avgCompetitorPrice = weightSum > 0 ? weightedPriceSum / weightSum : targetProduct.price;
3286 }
3287
3288 const rpi = avgCompetitorPrice > 0 ? (targetProduct.price / avgCompetitorPrice) * 100 : 100;
3289
3290 // Используем эластичность для оптимизации прибыли
3291 const usedElasticity = elasticity !== null ? elasticity : -1.2;
3292
3293 // Функция для расчета ожидаемой прибыли при заданной цене
3294 function calculateExpectedProfit(price) {
3295 if (!productCosts) return null;
3296
3297 const priceChange = (price - targetProduct.price) / targetProduct.price;
3298 const ordersChange = usedElasticity * priceChange;
3299 const expectedOrders = Math.max(1, targetProduct.orders * (1 + ordersChange));
3300
3301 const basePrice = price / (1 - ozonDiscount / 100);
3302 const sellerRevenue = basePrice * (1 - productCosts.commission) * expectedOrders;
3303 const expenses = (productCosts.cost + productCosts.delivery) * expectedOrders;
3304
3305 return sellerRevenue - expenses;
3306 }
3307
3308 // Рассчитываем 4 варианта цен с учетом эластичности и максимизации прибыли
3309 // Захват рынка: ищем оптимальную цену ниже текущей для РОСТА прибыли
3310 // Цена ОБЯЗАТЕЛЬНО ниже текущей, но прибыль должна РАСТИ за счет увеличения объема
3311 let marketCapturePrice = targetProduct.price;
3312 if (productCosts) {
3313 const currentProfit = calculateExpectedProfit(targetProduct.price);
3314 let bestPrice = targetProduct.price;
3315 let maxProfit = currentProfit;
3316
3317 // Ищем оптимум между -30% и -1% от текущей цены
3318 for (let multiplier = 0.70; multiplier < 0.99; multiplier += 0.01) {
3319 const testPrice = targetProduct.price * multiplier;
3320 const testProfit = calculateExpectedProfit(testPrice);
3321
3322 // Выбираем цену с максимальной прибылью (больше текущей)
3323 if (testProfit > maxProfit) {
3324 maxProfit = testProfit;
3325 bestPrice = testPrice;
3326 }
3327 }
3328
3329 // Если нашли цену с большей прибылью - используем её
3330 if (maxProfit > currentProfit) {
3331 marketCapturePrice = Math.round(bestPrice);
3332 } else {
3333 // Если не нашли - используем безопасное снижение на 5%
3334 marketCapturePrice = Math.round(targetProduct.price * 0.95);
3335 }
3336 } else {
3337 // Если нет данных о расходах, снижаем на 5%
3338 marketCapturePrice = Math.round(targetProduct.price * 0.95);
3339 }
3340
3341 // Агрессивная: ищем цену для максимизации прибыли с небольшим снижением
3342 // Допускаем падение прибыли до -2%, но стремимся к росту
3343 let aggressivePrice = targetProduct.price;
3344 if (productCosts) {
3345 const currentProfit = calculateExpectedProfit(targetProduct.price);
3346 let maxProfit = currentProfit;
3347 const minAcceptableProfit = currentProfit * 0.98; // Допускаем падение до -2%
3348
3349 // Ищем оптимум между -10% и +5% от текущей цены
3350 for (let multiplier = 0.90; multiplier <= 1.05; multiplier += 0.01) {
3351 const testPrice = targetProduct.price * multiplier;
3352 const testProfit = calculateExpectedProfit(testPrice);
3353
3354 // Выбираем цену с максимальной прибылью, но не ниже минимально допустимой
3355 if (testProfit >= minAcceptableProfit && testProfit > maxProfit) {
3356 maxProfit = testProfit;
3357 aggressivePrice = testPrice;
3358 }
3359 }
3360 aggressivePrice = Math.round(aggressivePrice);
3361 } else {
3362 // Если нет данных о расходах, умеренное снижение
3363 aggressivePrice = Math.round(targetProduct.price * 0.95);
3364 }
3365
3366 // Оптимальная: цена для максимизации прибыли (выше текущей)
3367 // Ищем оптимум между текущей ценой и +30%
3368 let optimalPrice = targetProduct.price;
3369 if (productCosts) {
3370 let maxProfit = calculateExpectedProfit(targetProduct.price);
3371 for (let multiplier = 1.05; multiplier <= 1.30; multiplier += 0.01) {
3372 const testPrice = targetProduct.price * multiplier;
3373 const testProfit = calculateExpectedProfit(testPrice);
3374 if (testProfit > maxProfit) {
3375 maxProfit = testProfit;
3376 optimalPrice = testPrice;
3377 }
3378 }
3379 optimalPrice = Math.round(optimalPrice);
3380 } else {
3381 // Если нет данных о расходах, используем рыночную цену
3382 optimalPrice = Math.round(Math.max(targetProduct.price * 1.10, marketOptimalPrice));
3383 }
3384
3385 // Рассчитываем базовые цены (цены поручения)
3386 const currentBasePrice = targetProduct.price / (1 - ozonDiscount / 100);
3387 const marketCaptureBasePrice = marketCapturePrice / (1 - ozonDiscount / 100);
3388 const aggressiveBasePrice = aggressivePrice / (1 - ozonDiscount / 100);
3389 const optimalBasePrice = optimalPrice / (1 - ozonDiscount / 100);
3390
3391 return {
3392 marketCapture: marketCapturePrice,
3393 aggressive: aggressivePrice,
3394 optimal: optimalPrice,
3395 currentPrice: targetProduct.price,
3396 currentBasePrice: Math.round(currentBasePrice),
3397 marketCaptureBasePrice: Math.round(marketCaptureBasePrice),
3398 aggressiveBasePrice: Math.round(aggressiveBasePrice),
3399 optimalBasePrice: Math.round(optimalBasePrice),
3400 currentPosition: targetProduct.position,
3401 currentProfit: null,
3402 rpi: rpi.toFixed(1),
3403 priceChange: {
3404 marketCapture: Math.round(((marketCapturePrice - targetProduct.price) / targetProduct.price * 100)),
3405 aggressive: Math.round(((aggressivePrice - targetProduct.price) / targetProduct.price * 100)),
3406 optimal: Math.round(((optimalPrice - targetProduct.price) / targetProduct.price * 100))
3407 },
3408 forecast: {
3409 marketCapture: calculateForecast(marketCapturePrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts),
3410 aggressive: calculateForecast(aggressivePrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts),
3411 optimal: calculateForecast(optimalPrice, targetProduct.price, targetProduct.orders, targetProduct.revenue, productCosts)
3412 }
3413 };
3414 }
3415
3416 // Общие рекомендации
3417 let weightedSum = 0;
3418 let weightSum = 0;
3419 products.forEach(p => {
3420 const positionBonus = p.position <= 5 ? 2 : 1;
3421 const weight = p.revenue * positionBonus;
3422 weightedSum += p.price * weight;
3423 weightSum += weight;
3424 });
3425 const weightedPrice = weightSum > 0 ? weightedSum / weightSum : 0;
3426
3427 const productsWithConversion = products.map(p => ({
3428 ...p,
3429 conversion: p.orders / p.price
3430 })).sort((a, b) => b.conversion - a.conversion);
3431 const topConversionPrice = productsWithConversion.length > 0
3432 ? productsWithConversion.slice(0, 3).reduce((sum, p) => sum + p.price, 0) / 3
3433 : 0;
3434
3435 const top10Prices = products.slice(0, 10).map(p => p.price).sort((a, b) => a - b);
3436 const medianPrice = top10Prices.length > 0
3437 ? top10Prices[Math.floor(top10Prices.length / 2)]
3438 : 0;
3439
3440 const baseOptimalPrice = (weightedPrice * 0.4 + topConversionPrice * 0.3 + medianPrice * 0.3);
3441
3442 const recommendedPrices = {
3443 marketCapture: {
3444 price: Math.round(baseOptimalPrice * 0.70),
3445 strategy: 'Захват рынка',
3446 description: 'Оптимальная цена ниже текущей для максимизации прибыли'
3447 },
3448 aggressive: {
3449 price: Math.round(baseOptimalPrice * 0.85),
3450 strategy: 'Агрессивная',
3451 description: 'Низкая цена для максимальных продаж и быстрого роста позиций'
3452 },
3453 optimal: {
3454 price: Math.round(baseOptimalPrice),
3455 strategy: 'Оптимальная',
3456 description: 'Баланс между прибылью и объемом продаж'
3457 }
3458 };
3459
3460 // Раздельные рекомендации для целевых брендов
3461 const glsProducts = products.filter(p => p.brand.includes('GLS Pharmaceuticals'));
3462 const skinphoriaProducts = products.filter(p => p.brand.includes('Skinphoria'));
3463
3464 const brandRecommendations = {};
3465
3466 // Рекомендации для GLS Pharmaceuticals
3467 if (glsProducts.length > 0) {
3468 const glsAvgPosition = glsProducts.reduce((sum, p) => sum + p.position, 0) / glsProducts.length;
3469 const glsAvgPrice = glsProducts.reduce((sum, p) => sum + p.price, 0) / glsProducts.length;
3470
3471 brandRecommendations.gls = {
3472 brand: 'GLS Pharmaceuticals',
3473 currentProducts: glsProducts.map(p => {
3474 const priceRec = calculatePriceForProduct(p, products);
3475 return {
3476 sku: p.sku,
3477 name: p.name,
3478 position: p.position,
3479 price: p.price,
3480 revenue: p.revenue,
3481 orders: p.orders,
3482 recommendations: priceRec
3483 };
3484 }),
3485 avgPosition: Math.round(glsAvgPosition),
3486 avgPrice: Math.round(glsAvgPrice)
3487 };
3488 }
3489
3490 // Рекомендации для Skinphoria
3491 if (skinphoriaProducts.length > 0) {
3492 const skinphoriaAvgPosition = skinphoriaProducts.reduce((sum, p) => sum + p.position, 0) / skinphoriaProducts.length;
3493 const skinphoriaAvgPrice = skinphoriaProducts.reduce((sum, p) => sum + p.price, 0) / skinphoriaProducts.length;
3494
3495 brandRecommendations.skinphoria = {
3496 brand: 'Skinphoria',
3497 currentProducts: skinphoriaProducts.map(p => {
3498 const priceRec = calculatePriceForProduct(p, products);
3499 return {
3500 sku: p.sku,
3501 name: p.name,
3502 position: p.position,
3503 price: p.price,
3504 revenue: p.revenue,
3505 orders: p.orders,
3506 recommendations: priceRec
3507 };
3508 }),
3509 avgPosition: Math.round(skinphoriaAvgPosition),
3510 avgPrice: Math.round(skinphoriaAvgPrice)
3511 };
3512 }
3513
3514 console.log('Ozon Product Parser: Analysis completed successfully');
3515
3516 return {
3517 totalRevenue,
3518 totalOrders,
3519 avgPrice: Math.round(totalRevenue / totalOrders),
3520 topProducts,
3521 priceSegments: segments,
3522 elasticity: elasticity !== null ? {
3523 value: elasticity.toFixed(2),
3524 interpretation: elasticityInterpretation
3525 } : null,
3526 recommendedPrices,
3527 brandRecommendations
3528 };
3529 } catch (error) {
3530 console.error('Ozon Product Parser: Error in analyzeProducts:', error);
3531 return null;
3532 }
3533 }
3534
3535 // Показываем результаты
3536 async function showResultsModal() {
3537 console.log('Ozon Product Parser: Opening results modal');
3538
3539 try {
3540 // Получаем результаты по спискам
3541 const allListResultsJson = await GM.getValue('ozon_parser_list_results', '{}');
3542 const allListResults = JSON.parse(allListResultsJson);
3543 const listNames = Object.keys(allListResults);
3544
3545 if (listNames.length === 0) {
3546 alert('Нет сохраненных результатов. Сначала выполните парсинг.');
3547 return;
3548 }
3549
3550 const modal = document.createElement('div');
3551 modal.className = 'ozon-parser-modal';
3552
3553 const content = document.createElement('div');
3554 content.className = 'ozon-parser-modal-content';
3555 content.style.maxWidth = '95vw';
3556 content.style.width = '95vw';
3557
3558 content.innerHTML = `
3559 <div class="ozon-parser-modal-header">Результаты парсинга</div>
3560 <div class="ozon-parser-modal-body">
3561 <div style="margin-bottom: 20px;">
3562 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Выберите список:</label>
3563 <div style="display: flex; gap: 10px; align-items: center;">
3564 <select class="ozon-parser-search" id="list-selector" style="flex: 1; margin-bottom: 0;">
3565 ${listNames.map(listName => {
3566 const list = allListResults[listName];
3567 const date = new Date(list.updatedAt).toLocaleDateString('ru-RU');
3568 const queriesCount = Object.keys(list.queries).length;
3569 return `<option value="${listName}">${listName} (${queriesCount} запросов, обновлен: ${date})</option>`;
3570 }).join('')}
3571 </select>
3572 <button class="ozon-parser-btn" id="delete-list-btn" style="background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);">Удалить список</button>
3573 </div>
3574 </div>
3575 <div style="margin-bottom: 15px;">
3576 <label style="font-size: 14px; font-weight: 600; margin-bottom: 8px; display: block;">Сортировка запросов:</label>
3577 <select class="ozon-parser-search" id="query-sort-selector" style="margin-bottom: 0;">
3578 <option value="revenue">По нашей выручке (убывание)</option>
3579 <option value="alphabet">По алфавиту (А-Я)</option>
3580 </select>
3581 </div>
3582 <input type="text" class="ozon-parser-search" id="query-search" placeholder="Поиск по запросам">
3583 <input type="text" class="ozon-parser-search" id="sku-search" placeholder="Поиск по SKU">
3584 <div class="ozon-parser-tabs" id="query-tabs"></div>
3585 <div id="results-container"></div>
3586 </div>
3587 <div class="ozon-parser-modal-footer">
3588 <button class="ozon-parser-btn" id="close-results-btn">Закрыть</button>
3589 </div>
3590 `;
3591
3592 modal.appendChild(content);
3593 document.body.appendChild(modal);
3594
3595 // Текущий выбранный список
3596 let currentListName = listNames[0];
3597 let currentResults = allListResults[currentListName].queries;
3598 let currentSortMode = 'revenue';
3599
3600 // Функция для сортировки запросов
3601 function sortQueries(queries, results, sortMode) {
3602 if (sortMode === 'alphabet') {
3603 return queries.sort((a, b) => a.localeCompare(b, 'ru'));
3604 } else {
3605 // Сортировка по нашей выручке
3606 return queries.sort((a, b) => {
3607 const productsA = results[a];
3608 const productsB = results[b];
3609
3610 // Находим наши товары и их выручку
3611 const ourProductsA = productsA.filter(p => p.isTargetBrand);
3612 const ourProductsB = productsB.filter(p => p.isTargetBrand);
3613
3614 const revenueA = ourProductsA.reduce((sum, p) => sum + p.revenue, 0);
3615 const revenueB = ourProductsB.reduce((sum, p) => sum + p.revenue, 0);
3616
3617 return revenueB - revenueA; // По убыванию
3618 });
3619 }
3620 }
3621
3622 // Функция для обновления отображения
3623 async function updateDisplay() {
3624 const queries = Object.keys(currentResults);
3625 const sortedQueries = sortQueries(queries, currentResults, currentSortMode);
3626
3627 // Создаем вкладки
3628 createTabs(sortedQueries, currentResults, content);
3629
3630 // Показываем результаты первого запроса
3631 if (sortedQueries.length > 0) {
3632 await displayResults(sortedQueries[0], currentResults[sortedQueries[0]], content);
3633 }
3634 }
3635
3636 // Обработчик выбора списка
3637 const listSelector = content.querySelector('#list-selector');
3638 listSelector.addEventListener('change', () => {
3639 currentListName = listSelector.value;
3640 currentResults = allListResults[currentListName].queries;
3641 updateDisplay();
3642 });
3643
3644 // Обработчик изменения сортировки
3645 const querySortSelector = content.querySelector('#query-sort-selector');
3646 querySortSelector.addEventListener('change', () => {
3647 currentSortMode = querySortSelector.value;
3648 updateDisplay();
3649 });
3650
3651 // Обработчик удаления списка
3652 const deleteListBtn = content.querySelector('#delete-list-btn');
3653 deleteListBtn.addEventListener('click', async () => {
3654 if (!confirm(`Вы уверены, что хотите удалить список "${currentListName}"?`)) {
3655 return;
3656 }
3657
3658 // Удаляем список
3659 delete allListResults[currentListName];
3660 await GM.setValue('ozon_parser_list_results', JSON.stringify(allListResults));
3661
3662 // Также удаляем из сохраненных списков
3663 const savedListsJson = await GM.getValue('ozon_parser_saved_lists', '{}');
3664 const savedLists = JSON.parse(savedListsJson);
3665 delete savedLists[currentListName];
3666 await GM.setValue('ozon_parser_saved_lists', JSON.stringify(savedLists));
3667
3668 console.log(`Ozon Product Parser: List "${currentListName}" deleted`);
3669
3670 // Обновляем UI
3671 const remainingLists = Object.keys(allListResults);
3672 if (remainingLists.length === 0) {
3673 alert('Все списки удалены');
3674 modal.remove();
3675 return;
3676 }
3677
3678 // Переключаемся на первый оставшийся список
3679 currentListName = remainingLists[0];
3680 currentResults = allListResults[currentListName].queries;
3681
3682 // Обновляем селектор
3683 listSelector.innerHTML = remainingLists.map(listName => {
3684 const list = allListResults[listName];
3685 const date = new Date(list.updatedAt).toLocaleDateString('ru-RU');
3686 const queriesCount = Object.keys(list.queries).length;
3687 return `<option value="${listName}">${listName} (${queriesCount} запросов, обновлен: ${date})</option>`;
3688 }).join('');
3689
3690 updateDisplay();
3691 });
3692
3693 // Обработчик закрытия модального окна
3694 const closeBtn = content.querySelector('#close-results-btn');
3695 closeBtn.addEventListener('click', () => {
3696 modal.remove();
3697 });
3698
3699 // Обработчик поиска по запросам
3700 const querySearchInput = content.querySelector('#query-search');
3701 querySearchInput.addEventListener('input', debounce(() => {
3702 const searchValue = querySearchInput.value.trim().toLowerCase();
3703 const queries = Object.keys(currentResults);
3704 const sortedQueries = sortQueries(queries, currentResults, currentSortMode);
3705 filterQueriesByQuery(searchValue, sortedQueries, currentResults, content);
3706 }, 300));
3707
3708 // Обработчик поиска по SKU
3709 const skuSearchInput = content.querySelector('#sku-search');
3710 skuSearchInput.addEventListener('input', debounce(() => {
3711 const searchValue = skuSearchInput.value.trim();
3712 const queries = Object.keys(currentResults);
3713 const sortedQueries = sortQueries(queries, currentResults, currentSortMode);
3714 filterQueriesBySKU(searchValue, sortedQueries, currentResults, content);
3715 }, 300));
3716
3717 // Создаем вкладки для запросов
3718 const initialQueries = Object.keys(currentResults);
3719 const sortedInitialQueries = sortQueries(initialQueries, currentResults, currentSortMode);
3720 createTabs(sortedInitialQueries, currentResults, content);
3721
3722 // Показываем результаты первого запроса
3723 await displayResults(sortedInitialQueries[0], currentResults[sortedInitialQueries[0]], content);
3724
3725 // Закрытие по клику на фон
3726 modal.addEventListener('click', (e) => {
3727 if (e.target === modal) {
3728 modal.remove();
3729 }
3730 });
3731
3732 console.log('Ozon Product Parser: Results modal shown');
3733 } catch (error) {
3734 console.error('Ozon Product Parser: Error in showResultsModal:', error);
3735 alert('Ошибка при отображении результатов: ' + error.message);
3736 }
3737 }
3738
3739 // Фильтруем запросы по названию запроса
3740 async function filterQueriesByQuery(queryText, allQueries, results, modalContent) {
3741 if (!queryText) {
3742 createTabs(allQueries, results, modalContent);
3743 await displayResults(allQueries[0], results[allQueries[0]], modalContent);
3744 return;
3745 }
3746
3747 const filteredQueries = allQueries.filter(q =>
3748 q.toLowerCase().includes(queryText)
3749 );
3750
3751 if (filteredQueries.length === 0) {
3752 const container = modalContent.querySelector('#results-container');
3753 container.innerHTML = '<p>Запросы не найдены</p>';
3754 const tabsContainer = modalContent.querySelector('#query-tabs');
3755 tabsContainer.innerHTML = '';
3756 return;
3757 }
3758
3759 createTabs(filteredQueries, results, modalContent);
3760 await displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
3761 }
3762
3763 // Фильтруем запросы по SKU
3764 async function filterQueriesBySKU(sku, allQueries, results, modalContent) {
3765 if (!sku) {
3766 createTabs(allQueries, results, modalContent);
3767 await displayResults(allQueries[0], results[allQueries[0]], modalContent);
3768 return;
3769 }
3770
3771 const filteredQueries = allQueries.filter(query => {
3772 const products = results[query];
3773 return products.some(product => product.sku.includes(sku));
3774 });
3775
3776 if (filteredQueries.length === 0) {
3777 const container = modalContent.querySelector('#results-container');
3778 container.innerHTML = '<p>Товары с таким SKU не найдены ни в одном запросе</p>';
3779 const tabsContainer = modalContent.querySelector('#query-tabs');
3780 tabsContainer.innerHTML = '';
3781 return;
3782 }
3783
3784 createTabs(filteredQueries, results, modalContent);
3785 await displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
3786 }
3787
3788 // Создаем вкладки для запросов
3789 function createTabs(queries, results, modalContent) {
3790 const tabsContainer = modalContent.querySelector('#query-tabs');
3791 tabsContainer.innerHTML = '';
3792
3793 queries.forEach((query, index) => {
3794 const tab = document.createElement('button');
3795 tab.className = 'ozon-parser-tab' + (index === 0 ? ' active' : '');
3796
3797 // Получаем позицию по выручке
3798 const products = results[query];
3799 const position = getOurRevenuePosition(query, products);
3800
3801 // Формируем текст вкладки
3802 if (position !== null) {
3803 tab.textContent = `${query} (${position} место)`;
3804 } else {
3805 tab.textContent = `${query} (нет наших)`;
3806 }
3807
3808 tab.addEventListener('click', async () => {
3809 tabsContainer.querySelectorAll('.ozon-parser-tab').forEach(t => t.classList.remove('active'));
3810 tab.classList.add('active');
3811 await displayResults(query, results[query], modalContent);
3812 });
3813 tabsContainer.appendChild(tab);
3814 });
3815
3816 // Функция для получения нашей позиции по выручке в запросе
3817 function getOurRevenuePosition(query, products) {
3818 // Сортируем все товары по выручке
3819 const sortedByRevenue = [...products].sort((a, b) => b.revenue - a.revenue);
3820
3821 // Находим первый наш товар
3822 const ourProductIndex = sortedByRevenue.findIndex(p => p.isTargetBrand);
3823
3824 if (ourProductIndex === -1) {
3825 return null; // Наших товаров нет
3826 }
3827
3828 return ourProductIndex + 1; // Позиция (1-based)
3829 }
3830 }
3831
3832 // Отображаем результаты для конкретного запроса
3833 async function displayResults(query, productsData, modalContent) {
3834 console.log('Ozon Product Parser: Displaying results for query:', query);
3835
3836 const container = modalContent.querySelector('#results-container');
3837
3838 try {
3839 let products;
3840
3841 if (Array.isArray(productsData)) {
3842 products = productsData;
3843 } else {
3844 container.innerHTML = '<p>Запросы не найдены</p>';
3845 return;
3846 }
3847
3848 if (!products || products.length === 0) {
3849 container.innerHTML = '<p>Нет данных для этого запроса</p>';
3850 return;
3851 }
3852
3853 // Получаем аналитику
3854 const analytics = await analyzeProducts(products);
3855
3856 if (!analytics) {
3857 container.innerHTML = '<p>Ошибка при анализе данных</p>';
3858 return;
3859 }
3860
3861 // Получаем скидку Ozon для расчета базовой цены
3862 const ozonDiscount = await GM.getValue('ozon_parser_discount', 50);
3863
3864 let tableHTML = `
3865 <table class="ozon-parser-results-table">
3866 <thead>
3867 <tr>
3868 <th>Позиция</th>
3869 <th>SKU</th>
3870 <th>Название</th>
3871 <th>Бренд</th>
3872 <th>Текущая цена</th>
3873 <th>Средняя цена</th>
3874 <th>Выручка</th>
3875 <th>Заказы</th>
3876 </tr>
3877 </thead>
3878 <tbody>
3879 `;
3880
3881 products.forEach(product => {
3882 const rowClass = product.isTargetBrand ? ' class="ozon-parser-highlight"' : '';
3883 const skuLink = `https://www.ozon.ru/product/${product.sku}`;
3884 const basePrice = product.price / (1 - ozonDiscount / 100);
3885 const avgPrice = product.orders > 0 ? product.revenue / product.orders : 0;
3886
3887 tableHTML += `
3888 <tr${rowClass}>
3889 <td>${product.position}</td>
3890 <td><a href="${skuLink}" target="_blank" class="ozon-parser-sku-link">${product.sku}</a></td>
3891 <td>${product.name || '—'}</td>
3892 <td>${product.brand}</td>
3893 <td>
3894 <div style="font-weight: 600;">${product.price.toLocaleString('ru-RU')} ₽</div>
3895 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${Math.round(basePrice).toLocaleString('ru-RU')} ₽</div>
3896 </td>
3897 <td>
3898 <div style="font-weight: 600;">${Math.round(avgPrice).toLocaleString('ru-RU')} ₽</div>
3899 </td>
3900 <td>${product.revenue.toLocaleString('ru-RU')} ₽</td>
3901 <td>${product.orders.toLocaleString('ru-RU')}</td>
3902 </tr>
3903 `;
3904 });
3905
3906 tableHTML += `
3907 </tbody>
3908 </table>
3909 `;
3910
3911 // Добавляем аналитику
3912 tableHTML += `
3913 <div class="ozon-parser-analytics">
3914 <div class="ozon-parser-analytics-header">📊 Аналитика запроса</div>
3915
3916 ${analytics.elasticity ? `
3917 <div class="ozon-parser-analytics-section">
3918 <div class="ozon-parser-analytics-card">
3919 <div class="ozon-parser-analytics-card-title">Эластичность запроса</div>
3920 <div class="ozon-parser-analytics-card-value">${analytics.elasticity.value}</div>
3921 <div style="font-size: 12px; color: #666; margin-top: 8px;">
3922 ${analytics.elasticity.interpretation}
3923 </div>
3924 </div>
3925 </div>
3926 ` : ''}
3927
3928 <div class="ozon-parser-analytics-section">
3929 <div class="ozon-parser-analytics-section-title">Рекомендованные цены</div>
3930 <div class="ozon-parser-analytics-grid">
3931 <div class="ozon-parser-analytics-card" style="border: 2px solid #6c757d;">
3932 <div class="ozon-parser-analytics-card-title">⚡ Захват рынка</div>
3933 <div class="ozon-parser-analytics-card-value" style="color: #6c757d;">${analytics.recommendedPrices.marketCapture.price.toLocaleString('ru-RU')} ₽</div>
3934 <div style="font-size: 12px; color: #666; margin-top: 8px;">Оптимальная цена ниже текущей для максимизации прибыли</div>
3935 </div>
3936 <div class="ozon-parser-analytics-card" style="border: 2px solid #dc3545;">
3937 <div class="ozon-parser-analytics-card-title">✅ Оптимальная</div>
3938 <div class="ozon-parser-analytics-card-value" style="color: #dc3545;">${analytics.recommendedPrices.aggressive.price.toLocaleString('ru-RU')} ₽</div>
3939 <div style="font-size: 12px; color: #666; margin-top: 8px;">Низкая цена для максимальных продаж и быстрого роста позиций</div>
3940 </div>
3941 <div class="ozon-parser-analytics-card" style="border: 2px solid #28a745;">
3942 <div class="ozon-parser-analytics-card-title">🔥 Агрессивная</div>
3943 <div class="ozon-parser-analytics-card-value" style="color: #28a745;">${analytics.recommendedPrices.optimal.price.toLocaleString('ru-RU')} ₽</div>
3944 <div style="font-size: 12px; color: #666; margin-top: 8px;">Баланс между прибылью и объемом продаж</div>
3945 </div>
3946 </div>
3947 <div class="ozon-parser-info">
3948 💡 Расчет учитывает: средневзвешенную цену по выручке (вес 40%), оптимальную конверсию цена/заказы (вес 30%), медианную цену топ-10 (вес 30%). Товары из топ-5 позиций имеют удвоенный вес.
3949 </div>
3950 </div>
3951
3952 ${analytics.brandRecommendations && Object.keys(analytics.brandRecommendations).length > 0 ? `
3953 <div class="ozon-parser-analytics-section">
3954 <div class="ozon-parser-analytics-section-title">🎯 Рекомендации для ваших брендов</div>
3955 ${analytics.brandRecommendations.gls ? `
3956 <div style="background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 2px solid #ffc107;">
3957 <div style="font-size: 18px; font-weight: 600; margin-bottom: 15px; color: #333;">
3958 ${analytics.brandRecommendations.gls.brand}
3959 </div>
3960 <div style="margin-bottom: 15px;">
3961 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Текущие товары в выдаче: ${analytics.brandRecommendations.gls.currentProducts.length}</div>
3962 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Средняя позиция: ${analytics.brandRecommendations.gls.avgPosition}</div>
3963 <div style="font-size: 13px; color: #666;">Средняя цена: ${analytics.brandRecommendations.gls.avgPrice.toLocaleString('ru-RU')} ₽</div>
3964 </div>
3965 <details style="margin-top: 10px;" open>
3966 <summary style="cursor: pointer; font-size: 13px; color: #005bff; font-weight: 600;">Показать товары</summary>
3967 <table class="ozon-parser-analytics-table" style="margin-top: 10px;">
3968 <thead>
3969 <tr>
3970 <th>SKU</th>
3971 <th>Название</th>
3972 <th>Позиция</th>
3973 <th>Текущая цена</th>
3974 <th>⚡ Захват рынка</th>
3975 <th>✅ Оптимальная</th>
3976 <th>🔥 Агрессивная</th>
3977 </tr>
3978 </thead>
3979 <tbody>
3980 ${analytics.brandRecommendations.gls.currentProducts.map(p => {
3981 const rec = p.recommendations;
3982 if (!rec) return `
3983 <tr>
3984 <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
3985 <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
3986 <td>${p.position}</td>
3987 <td>${p.price.toLocaleString('ru-RU')} ₽</td>
3988 <td>—</td>
3989 <td>—</td>
3990 <td>—</td>
3991 </tr>
3992 `;
3993 return `
3994 <tr>
3995 <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
3996 <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
3997 <td>${p.position}</td>
3998 <td>
3999 <div style="font-weight: 600;">${p.price.toLocaleString('ru-RU')} ₽</div>
4000 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.currentBasePrice.toLocaleString('ru-RU')} ₽</div>
4001 <div style="font-size: 11px; color: #666; margin-top: 4px;">Выручка: ${p.revenue.toLocaleString('ru-RU')} ₽</div>
4002 <div style="font-size: 11px; color: #666;">Заказы: ${p.orders.toLocaleString('ru-RU')}</div>
4003 ${rec.skuDiscount ? `
4004 <div style="font-size: 11px; color: #ff6b00; font-weight: 600; margin-top: 4px;">
4005 Скидка Ozon: ${rec.skuDiscount}%
4006 </div>
4007 ` : ''}
4008 ${rec.currentProfit !== null ? `
4009 <div style="font-size: 11px; color: #005bff; font-weight: 600; margin-top: 4px;">
4010 Прибыль: ${rec.currentProfit.toFixed(0)} ₽
4011 </div>
4012 ` : ''}
4013 </td>
4014 <td style="background: #e9ecef;">
4015 <div style="font-weight: 600; color: #6c757d;">${rec.marketCapture.toLocaleString('ru-RU')} ₽</div>
4016 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.marketCaptureBasePrice.toLocaleString('ru-RU')} ₽</div>
4017 <div style="font-size: 11px; color: ${rec.priceChange.marketCapture < 0 ? '#dc3545' : '#28a745'};">
4018 ${rec.priceChange.marketCapture > 0 ? '+' : ''}${rec.priceChange.marketCapture}%
4019 </div>
4020 ${rec.forecast && rec.forecast.marketCapture ? `
4021 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4022 Выручка: ${rec.forecast.marketCapture.revenueChange > 0 ? '+' : ''}${rec.forecast.marketCapture.revenueChange}%
4023 </div>
4024 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4025 Заказы: ${rec.forecast.marketCapture.ordersChange > 0 ? '+' : ''}${rec.forecast.marketCapture.ordersChange}%
4026 </div>
4027 ${rec.forecast.marketCapture.profit !== null ? `
4028 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4029 Прибыль: ${rec.forecast.marketCapture.profitChange > 0 ? '+' : ''}${rec.forecast.marketCapture.profitChange}%
4030 </div>
4031 <div style="font-size: 10px; color: #666;">
4032 ${rec.forecast.marketCapture.profit.toFixed(0)} ₽
4033 </div>
4034 ` : ''}
4035 ` : ''}
4036 </td>
4037 <td style="background: #d4edda;">
4038 <div style="font-weight: 600; color: #28a745;">${rec.aggressive.toLocaleString('ru-RU')} ₽</div>
4039 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.aggressiveBasePrice.toLocaleString('ru-RU')} ₽</div>
4040 <div style="font-size: 11px; color: ${rec.priceChange.aggressive < 0 ? '#dc3545' : '#28a745'};">
4041 ${rec.priceChange.aggressive > 0 ? '+' : ''}${rec.priceChange.aggressive}%
4042 </div>
4043 ${rec.forecast && rec.forecast.aggressive ? `
4044 <div style="font-size: 11px; color: ${rec.forecast.aggressive.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4045 Выручка: ${rec.forecast.aggressive.revenueChange > 0 ? '+' : ''}${rec.forecast.aggressive.revenueChange}%
4046 </div>
4047 <div style="font-size: 11px; color: ${rec.forecast.aggressive.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4048 Заказы: ${rec.forecast.aggressive.ordersChange > 0 ? '+' : ''}${rec.forecast.aggressive.ordersChange}%
4049 </div>
4050 ${rec.forecast.aggressive.profit !== null ? `
4051 <div style="font-size: 11px; color: ${rec.forecast.aggressive.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4052 Прибыль: ${rec.forecast.aggressive.profitChange > 0 ? '+' : ''}${rec.forecast.aggressive.profitChange}%
4053 </div>
4054 <div style="font-size: 10px; color: #666;">
4055 ${rec.forecast.aggressive.profit.toFixed(0)} ₽
4056 </div>
4057 ` : ''}
4058 ` : ''}
4059 </td>
4060 <td style="background: #fff3cd;">
4061 <div style="font-weight: 600; color: #dc3545;">${rec.optimal.toLocaleString('ru-RU')} ₽</div>
4062 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.optimalBasePrice.toLocaleString('ru-RU')} ₽</div>
4063 <div style="font-size: 11px; color: ${rec.priceChange.optimal < 0 ? '#dc3545' : '#28a745'};">
4064 ${rec.priceChange.optimal > 0 ? '+' : ''}${rec.priceChange.optimal}%
4065 </div>
4066 ${rec.forecast && rec.forecast.optimal ? `
4067 <div style="font-size: 11px; color: ${rec.forecast.optimal.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4068 Выручка: ${rec.forecast.optimal.revenueChange > 0 ? '+' : ''}${rec.forecast.optimal.revenueChange}%
4069 </div>
4070 <div style="font-size: 11px; color: ${rec.forecast.optimal.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4071 Заказы: ${rec.forecast.optimal.ordersChange > 0 ? '+' : ''}${rec.forecast.optimal.ordersChange}%
4072 </div>
4073 ${rec.forecast.optimal.profit !== null ? `
4074 <div style="font-size: 11px; color: ${rec.forecast.optimal.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4075 Прибыль: ${rec.forecast.optimal.profitChange > 0 ? '+' : ''}${rec.forecast.optimal.profitChange}%
4076 </div>
4077 <div style="font-size: 10px; color: #666;">
4078 ${rec.forecast.optimal.profit.toFixed(0)} ₽
4079 </div>
4080 ` : ''}
4081 ` : ''}
4082 </td>
4083 </tr>
4084 `;
4085 }).join('')}
4086 </tbody>
4087 </table>
4088 </details>
4089 </div>
4090 ` : ''}
4091 ${analytics.brandRecommendations.skinphoria ? `
4092 <div style="background: white; padding: 20px; border-radius: 8px; border: 2px solid #ffc107;">
4093 <div style="font-size: 18px; font-weight: 600; margin-bottom: 15px; color: #333;">
4094 ${analytics.brandRecommendations.skinphoria.brand}
4095 </div>
4096 <div style="margin-bottom: 15px;">
4097 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Текущие товары в выдаче: ${analytics.brandRecommendations.skinphoria.currentProducts.length}</div>
4098 <div style="font-size: 13px; color: #666; margin-bottom: 5px;">Средняя позиция: ${analytics.brandRecommendations.skinphoria.avgPosition}</div>
4099 <div style="font-size: 13px; color: #666;">Средняя цена: ${analytics.brandRecommendations.skinphoria.avgPrice.toLocaleString('ru-RU')} ₽</div>
4100 </div>
4101 <details style="margin-top: 10px;" open>
4102 <summary style="cursor: pointer; font-size: 13px; color: #005bff; font-weight: 600;">Показать товары</summary>
4103 <table class="ozon-parser-analytics-table" style="margin-top: 10px;">
4104 <thead>
4105 <tr>
4106 <th>SKU</th>
4107 <th>Название</th>
4108 <th>Позиция</th>
4109 <th>Текущая цена</th>
4110 <th>⚡ Захват рынка</th>
4111 <th>✅ Оптимальная</th>
4112 <th>🔥 Агрессивная</th>
4113 </tr>
4114 </thead>
4115 <tbody>
4116 ${analytics.brandRecommendations.skinphoria.currentProducts.map(p => {
4117 const rec = p.recommendations;
4118 if (!rec) return `
4119 <tr>
4120 <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
4121 <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
4122 <td>${p.position}</td>
4123 <td>${p.price.toLocaleString('ru-RU')} ₽</td>
4124 <td>—</td>
4125 <td>—</td>
4126 <td>—</td>
4127 </tr>
4128 `;
4129 return `
4130 <tr>
4131 <td><a href="https://www.ozon.ru/product/${p.sku}" target="_blank" class="ozon-parser-sku-link">${p.sku}</a></td>
4132 <td style="max-width: 200px; white-space: normal;">${p.name || '—'}</td>
4133 <td>${p.position}</td>
4134 <td>
4135 <div style="font-weight: 600;">${p.price.toLocaleString('ru-RU')} ₽</div>
4136 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.currentBasePrice.toLocaleString('ru-RU')} ₽</div>
4137 <div style="font-size: 11px; color: #666; margin-top: 4px;">Выручка: ${p.revenue.toLocaleString('ru-RU')} ₽</div>
4138 <div style="font-size: 11px; color: #666;">Заказы: ${p.orders.toLocaleString('ru-RU')}</div>
4139 ${rec.skuDiscount ? `
4140 <div style="font-size: 11px; color: #ff6b00; font-weight: 600; margin-top: 4px;">
4141 Скидка Ozon: ${rec.skuDiscount}%
4142 </div>
4143 ` : ''}
4144 ${rec.currentProfit !== null ? `
4145 <div style="font-size: 11px; color: #005bff; font-weight: 600; margin-top: 4px;">
4146 Прибыль: ${rec.currentProfit.toFixed(0)} ₽
4147 </div>
4148 ` : ''}
4149 </td>
4150 <td style="background: #e9ecef;">
4151 <div style="font-weight: 600; color: #6c757d;">${rec.marketCapture.toLocaleString('ru-RU')} ₽</div>
4152 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.marketCaptureBasePrice.toLocaleString('ru-RU')} ₽</div>
4153 <div style="font-size: 11px; color: ${rec.priceChange.marketCapture < 0 ? '#dc3545' : '#28a745'};">
4154 ${rec.priceChange.marketCapture > 0 ? '+' : ''}${rec.priceChange.marketCapture}%
4155 </div>
4156 ${rec.forecast && rec.forecast.marketCapture ? `
4157 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4158 Выручка: ${rec.forecast.marketCapture.revenueChange > 0 ? '+' : ''}${rec.forecast.marketCapture.revenueChange}%
4159 </div>
4160 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4161 Заказы: ${rec.forecast.marketCapture.ordersChange > 0 ? '+' : ''}${rec.forecast.marketCapture.ordersChange}%
4162 </div>
4163 ${rec.forecast.marketCapture.profit !== null ? `
4164 <div style="font-size: 11px; color: ${rec.forecast.marketCapture.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4165 Прибыль: ${rec.forecast.marketCapture.profitChange > 0 ? '+' : ''}${rec.forecast.marketCapture.profitChange}%
4166 </div>
4167 <div style="font-size: 10px; color: #666;">
4168 ${rec.forecast.marketCapture.profit.toFixed(0)} ₽
4169 </div>
4170 ` : ''}
4171 ` : ''}
4172 </td>
4173 <td style="background: #d4edda;">
4174 <div style="font-weight: 600; color: #28a745;">${rec.aggressive.toLocaleString('ru-RU')} ₽</div>
4175 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.aggressiveBasePrice.toLocaleString('ru-RU')} ₽</div>
4176 <div style="font-size: 11px; color: ${rec.priceChange.aggressive < 0 ? '#dc3545' : '#28a745'};">
4177 ${rec.priceChange.aggressive > 0 ? '+' : ''}${rec.priceChange.aggressive}%
4178 </div>
4179 ${rec.forecast && rec.forecast.aggressive ? `
4180 <div style="font-size: 11px; color: ${rec.forecast.aggressive.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4181 Выручка: ${rec.forecast.aggressive.revenueChange > 0 ? '+' : ''}${rec.forecast.aggressive.revenueChange}%
4182 </div>
4183 <div style="font-size: 11px; color: ${rec.forecast.aggressive.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4184 Заказы: ${rec.forecast.aggressive.ordersChange > 0 ? '+' : ''}${rec.forecast.aggressive.ordersChange}%
4185 </div>
4186 ${rec.forecast.aggressive.profit !== null ? `
4187 <div style="font-size: 11px; color: ${rec.forecast.aggressive.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4188 Прибыль: ${rec.forecast.aggressive.profitChange > 0 ? '+' : ''}${rec.forecast.aggressive.profitChange}%
4189 </div>
4190 <div style="font-size: 10px; color: #666;">
4191 ${rec.forecast.aggressive.profit.toFixed(0)} ₽
4192 </div>
4193 ` : ''}
4194 ` : ''}
4195 </td>
4196 <td style="background: #fff3cd;">
4197 <div style="font-weight: 600; color: #dc3545;">${rec.optimal.toLocaleString('ru-RU')} ₽</div>
4198 <div style="font-size: 10px; color: #999; margin-top: 2px;">База: ${rec.optimalBasePrice.toLocaleString('ru-RU')} ₽</div>
4199 <div style="font-size: 11px; color: ${rec.priceChange.optimal < 0 ? '#dc3545' : '#28a745'};">
4200 ${rec.priceChange.optimal > 0 ? '+' : ''}${rec.priceChange.optimal}%
4201 </div>
4202 ${rec.forecast && rec.forecast.optimal ? `
4203 <div style="font-size: 11px; color: ${rec.forecast.optimal.revenueChange >= 0 ? '#28a745' : '#dc3545'}; margin-top: 4px;">
4204 Выручка: ${rec.forecast.optimal.revenueChange > 0 ? '+' : ''}${rec.forecast.optimal.revenueChange}%
4205 </div>
4206 <div style="font-size: 11px; color: ${rec.forecast.optimal.ordersChange >= 0 ? '#28a745' : '#dc3545'};">
4207 Заказы: ${rec.forecast.optimal.ordersChange > 0 ? '+' : ''}${rec.forecast.optimal.ordersChange}%
4208 </div>
4209 ${rec.forecast.optimal.profit !== null ? `
4210 <div style="font-size: 11px; color: ${rec.forecast.optimal.profitChange >= 0 ? '#28a745' : '#dc3545'}; font-weight: 600; margin-top: 2px;">
4211 Прибыль: ${rec.forecast.optimal.profitChange > 0 ? '+' : ''}${rec.forecast.optimal.profitChange}%
4212 </div>
4213 <div style="font-size: 10px; color: #666;">
4214 ${rec.forecast.optimal.profit.toFixed(0)} ₽
4215 </div>
4216 ` : ''}
4217 ` : ''}
4218 </td>
4219 </tr>
4220 `;
4221 }).join('')}
4222 </tbody>
4223 </table>
4224 </details>
4225 </div>
4226 ` : ''}
4227 </div>
4228 ` : ''}
4229
4230 <div class="ozon-parser-analytics-section">
4231 <div class="ozon-parser-analytics-section-title">Общая статистика</div>
4232 <div class="ozon-parser-analytics-grid">
4233 <div class="ozon-parser-analytics-card">
4234 <div class="ozon-parser-analytics-card-title">Общая выручка</div>
4235 <div class="ozon-parser-analytics-card-value">${analytics.totalRevenue.toLocaleString('ru-RU')} ₽</div>
4236 </div>
4237 <div class="ozon-parser-analytics-card">
4238 <div class="ozon-parser-analytics-card-title">Всего заказов</div>
4239 <div class="ozon-parser-analytics-card-value">${analytics.totalOrders.toLocaleString('ru-RU')}</div>
4240 </div>
4241 <div class="ozon-parser-analytics-card">
4242 <div class="ozon-parser-analytics-card-title">Средний чек</div>
4243 <div class="ozon-parser-analytics-card-value">${analytics.avgPrice.toLocaleString('ru-RU')} ₽</div>
4244 </div>
4245 </div>
4246 </div>
4247
4248 <div class="ozon-parser-analytics-section">
4249 <div class="ozon-parser-analytics-section-title">Топ-5 товаров по выручке</div>
4250 <table class="ozon-parser-analytics-table">
4251 <thead>
4252 <tr>
4253 <th>Товар</th>
4254 <th>Бренд</th>
4255 <th>Цена</th>
4256 <th>Выручка</th>
4257 <th>Доля</th>
4258 </tr>
4259 </thead>
4260 <tbody>
4261 `;
4262
4263 analytics.topProducts.forEach(product => {
4264 tableHTML += `
4265 <tr>
4266 <td style="max-width: 300px; white-space: normal;">${product.name || '—'}</td>
4267 <td>${product.brand}</td>
4268 <td>${product.price.toLocaleString('ru-RU')} ₽</td>
4269 <td>${product.revenue.toLocaleString('ru-RU')} ₽</td>
4270 <td>${product.revenueShare}%</td>
4271 </tr>
4272 `;
4273 });
4274
4275 tableHTML += `
4276 </tbody>
4277 </table>
4278 </div>
4279
4280 <div class="ozon-parser-analytics-section">
4281 <div class="ozon-parser-analytics-section-title">Ценовые сегменты</div>
4282 <table class="ozon-parser-analytics-table">
4283 <thead>
4284 <tr>
4285 <th>Сегмент</th>
4286 <th>Диапазон цен</th>
4287 <th>Товаров</th>
4288 <th>Средняя цена</th>
4289 <th>Выручка</th>
4290 <th>Доля выручки</th>
4291 </tr>
4292 </thead>
4293 <tbody>
4294 `;
4295
4296 analytics.priceSegments.forEach(segment => {
4297 tableHTML += `
4298 <tr>
4299 <td>${segment.name}</td>
4300 <td>${segment.priceRange}</td>
4301 <td>${segment.count}</td>
4302 <td>${segment.avgPrice.toLocaleString('ru-RU')} ₽</td>
4303 <td>${segment.revenue.toLocaleString('ru-RU')} ₽</td>
4304 <td>
4305 <div style="display: flex; align-items: center; gap: 10px;">
4306 <div class="ozon-parser-analytics-bar" style="width: ${segment.revenueShare}%; min-width: 20px;"></div>
4307 <span>${segment.revenueShare}%</span>
4308 </div>
4309 </td>
4310 </tr>
4311 `;
4312 });
4313
4314 tableHTML += `
4315 </tbody>
4316 </table>
4317 </div>
4318 </div>
4319 `;
4320
4321 container.innerHTML = tableHTML;
4322 console.log('Ozon Product Parser: Results displayed successfully');
4323 } catch (error) {
4324 console.error('Ozon Product Parser: Error in displayResults:', error);
4325 container.innerHTML = '<p>Ошибка при отображении результатов: ' + error.message + '</p>';
4326 }
4327 }
4328
4329 // Инициализация
4330 function init() {
4331 console.log('Ozon Product Parser: Initializing...');
4332
4333 if (document.readyState === 'loading') {
4334 document.addEventListener('DOMContentLoaded', () => {
4335 addStyles();
4336 createUI();
4337 continueParsingIfActive();
4338 continueDiscountCalculationIfActive();
4339 });
4340 } else {
4341 addStyles();
4342 createUI();
4343 continueParsingIfActive();
4344 continueDiscountCalculationIfActive();
4345 }
4346 }
4347
4348 init();
4349})();