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