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