Analyzes MP Stats data and suggests optimal pricing using Bayesian methods to maximize profit
Size
65.0 KB
Version
1.2.77
Created
Dec 13, 2025
Updated
3 days ago
1// ==UserScript==
2// @name OZON Price Optimizer with Bayesian Analysis
3// @description Analyzes MP Stats data and suggests optimal pricing using Bayesian methods to maximize profit
4// @version 1.2.77
5// @match https://*.ozon.ru/*
6// @icon https://st.ozone.ru/assets/favicon.ico
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.xmlhttpRequest
10// @require https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js
11// ==/UserScript==
12(function() {
13 'use strict';
14
15 // Product data mapping (артикул -> себестоимость, комиссия, доставка)
16 const PRODUCT_DATA = {
17 '1740824669': { cost: 146.4, commission: 0.39, delivery: 105 },
18 '240637697': { cost: 159.6, commission: 0.39, delivery: 105 },
19
20 };
21
22 // Extract product ID from URL
23 function getProductId() {
24 const match = window.location.href.match(/product\/[^\/]+-(\d+)/);
25 return match ? match[1] : null;
26 }
27
28 // Get short SKU from full product ID (last 5 digits)
29 function getShortSku(fullProductId) {
30 // Try to find exact match first
31 if (PRODUCT_DATA[fullProductId]) {
32 return fullProductId;
33 }
34
35 // Try last 5 digits
36 const last5 = fullProductId.slice(-5);
37 if (PRODUCT_DATA[last5]) {
38 return last5;
39 }
40
41 // Try last 6 digits
42 const last6 = fullProductId.slice(-6);
43 if (PRODUCT_DATA[last6]) {
44 return last6;
45 }
46
47 return null;
48 }
49
50 // Extract daily price and sales data from MP Stats chart
51 async function extractDailyDataFromChart(widget) {
52 console.log('>>> extractDailyDataFromChart CALLED (v1.2.21 - REAL COORDINATES) <<<');
53
54 try {
55 const dailyData = [];
56
57 // Ищем SVG графики
58 const svgs = widget.querySelectorAll('svg.apexcharts-svg');
59 console.log('Found ApexCharts SVG elements:', svgs.length);
60
61 if (svgs.length === 0) {
62 console.log('No SVG charts found');
63 return dailyData;
64 }
65
66 // Берем первый график (график продаж и цены)
67 const salesSvg = svgs[0];
68 console.log('Analyzing sales chart (SVG 0)...');
69
70 // Ищем все path элементы с классом apexcharts-bar-area
71 const bars = salesSvg.querySelectorAll('path.apexcharts-bar-area');
72 console.log(`Found ${bars.length} bar elements`);
73
74 if (bars.length === 0) {
75 console.log('No bars found in sales chart');
76 return dailyData;
77 }
78
79 console.log('Starting tooltip extraction with real coordinates...');
80
81 // Извлекаем данные через симуляцию наведения мыши с реальными координатами
82 for (let index = 0; index < bars.length; index++) {
83 const bar = bars[index];
84
85 // Получаем координаты центра столбца
86 const rect = bar.getBoundingClientRect();
87 const centerX = rect.left + rect.width / 2;
88 const centerY = rect.top + rect.height / 2;
89
90 // Симулируем наведение мыши с реальными координатами
91 bar.dispatchEvent(new MouseEvent('mousemove', {
92 bubbles: true,
93 cancelable: true,
94 view: window,
95 clientX: centerX,
96 clientY: centerY
97 }));
98
99 // Ждем появления и обновления тултипа
100 await new Promise(resolve => setTimeout(resolve, 150));
101
102 // Ищем кастомный тултип MP Stats
103 const tooltip = document.querySelector('.chart-custom-tooltip');
104
105 if (!tooltip) {
106 console.log(`Day ${index + 1}: Custom tooltip not found`);
107 continue;
108 }
109
110 const tooltipText = tooltip.textContent;
111
112 // Извлекаем продажи
113 const salesMatch = tooltipText.match(/Продажи:\s*(\d+)\s*шт/);
114 const sales = salesMatch ? parseInt(salesMatch[1]) : null;
115
116 // Извлекаем цену
117 const priceMatch = tooltipText.match(/Цена:\s*(\d+)\s*₽/);
118 const price = priceMatch ? parseInt(priceMatch[1]) : null;
119
120 if (sales !== null && price !== null && sales > 0 && price > 0) {
121 dailyData.push({
122 day: index + 1,
123 sales: sales,
124 price: price
125 });
126 } else {
127 console.log(`Day ${index + 1}: Invalid data - sales: ${sales}, price: ${price}`);
128 }
129 }
130
131 console.log(`Total extracted daily data points: ${dailyData.length}`);
132 console.log('Sample data - First 5 days:', dailyData.slice(0, 5));
133 console.log('Sample data - Last 5 days:', dailyData.slice(-5));
134 return dailyData;
135
136 } catch (error) {
137 console.error('Error extracting daily data:', error);
138 return [];
139 }
140 }
141
142 // Extract stock data from MP Stats stock chart
143 function extractStockData() {
144 console.log('Extracting stock data from MP Stats...');
145
146 const mpsWidget = document.querySelector('.mps-sidebar');
147 if (!mpsWidget) {
148 console.error('MP Stats widget not found');
149 return null;
150 }
151
152 // Find all SVG charts
153 const svgs = mpsWidget.querySelectorAll('.vue-apexcharts svg');
154 if (!svgs || svgs.length < 2) {
155 console.error('Stock chart SVG not found');
156 return null;
157 }
158
159 // Second chart is the stock chart
160 const stockSvg = svgs[1];
161 const stockBars = stockSvg.querySelectorAll('.apexcharts-bar-area');
162
163 if (!stockBars || stockBars.length === 0) {
164 console.error('No stock bars found');
165 return null;
166 }
167
168 const stockPoints = [];
169 stockBars.forEach((bar, index) => {
170 const stock = parseInt(bar.getAttribute('val'));
171 if (!isNaN(stock)) {
172 stockPoints.push({
173 day: index + 1,
174 stock: stock
175 });
176 }
177 });
178
179 console.log(`Extracted ${stockPoints.length} days of stock data from chart`);
180 return stockPoints;
181 }
182
183 // Extract competitor data from product page
184 async function extractCompetitorData() {
185 console.log('=== START extractCompetitorData ===');
186 try {
187 console.log('Extracting competitor data...');
188
189 // Get current product name to identify competitors
190 console.log('Looking for product name element...');
191 const productNameElement = document.querySelector('[data-widget="webProductHeading"] h1');
192 console.log('Product name element found:', !!productNameElement);
193
194 if (!productNameElement) {
195 console.error('Product name not found');
196 return null;
197 }
198
199 const productName = productNameElement.textContent.trim();
200 console.log('Product name:', productName);
201
202 const words = productName.split(' ');
203 console.log('Words:', words);
204
205 // Get first 2 words for comparison
206 const firstWord = words[0].toLowerCase();
207 const secondWord = words[1].toLowerCase();
208 console.log('Searching for competitors with first word:', firstWord, 'and second word:', secondWord);
209
210 const competitors = [];
211
212 // Look for competitor products in recommendation sections
213 // Find all divs with class containing "bq03" (product name containers)
214 const productNameDivs = document.querySelectorAll('div[class*="bq03"]');
215 console.log(`Found ${productNameDivs.length} product name divs on page`);
216
217 const processedSkus = new Set();
218 const currentProductId = getProductId();
219
220 productNameDivs.forEach((nameDiv, index) => {
221 try {
222 // Get product name from span inside
223 const nameSpan = nameDiv.querySelector('span.tsBody500Medium');
224 if (!nameSpan) {
225 console.log(`Div ${index}: No name span found`);
226 return;
227 }
228
229 const competitorName = nameSpan.textContent.trim();
230 const competitorWords = competitorName.split(' ');
231 const competitorFirstWord = competitorWords[0].toLowerCase();
232 const competitorSecondWord = competitorWords[1] ? competitorWords[1].toLowerCase() : '';
233
234 console.log(`Div ${index}: ${competitorName} (first 2 words: ${competitorFirstWord} ${competitorSecondWord})`);
235
236 // Check if it's a competitor (same first 2 words)
237 if (competitorFirstWord !== firstWord || competitorSecondWord !== secondWord) {
238 console.log(`Div ${index}: Skipping - different words (${competitorFirstWord} ${competitorSecondWord} vs ${firstWord} ${secondWord})`);
239 return;
240 }
241
242 // Find the product link to get SKU
243 const container = nameDiv.closest('div[class*="tile"]') || nameDiv.closest('div');
244 if (!container) {
245 console.log(`Div ${index}: No container found`);
246 return;
247 }
248
249 const productLink = container.querySelector('a[href*="/product/"]');
250 if (!productLink) {
251 console.log(`Div ${index}: No product link found`);
252 return;
253 }
254
255 // Get SKU from link
256 const match = productLink.href.match(/product\/[^\/]+-(\d+)/);
257 if (!match) {
258 console.log(`Div ${index}: No SKU in link`);
259 return;
260 }
261
262 const sku = match[1];
263
264 // Skip current product and already processed
265 if (sku === currentProductId || processedSkus.has(sku)) {
266 console.log(`Div ${index}: Skipping - current product or already processed`);
267 return;
268 }
269 processedSkus.add(sku);
270
271 // Find price in the same container
272 const priceSpans = container.querySelectorAll('span');
273 let price = null;
274
275 for (const span of priceSpans) {
276 const text = span.textContent.trim();
277 // Look for price pattern (digits with optional spaces and ₽)
278 if (text.match(/^\d[\d\s]*₽?$/)) {
279 const priceText = text.replace(/[^\d]/g, '');
280 const parsedPrice = parseInt(priceText);
281 if (!isNaN(parsedPrice) && parsedPrice > 100 && parsedPrice < 10000) {
282 price = parsedPrice;
283 break;
284 }
285 }
286 }
287
288 if (!price) {
289 console.log(`Div ${index}: No valid price found`);
290 return;
291 }
292
293 // Try to get MP Stats data
294 let sales30days = null;
295 let revenue30days = null;
296
297 const mpstatsContainer = container.querySelector('[class*="container_ihu0b"], .mpstats-loader');
298 if (mpstatsContainer) {
299 const text = mpstatsContainer.textContent;
300
301 // Look for sales data
302 const salesMatch = text.match(/(\d+)\s*шт/);
303 if (salesMatch) {
304 sales30days = parseInt(salesMatch[1]);
305 // Calculate revenue as price x sales
306 revenue30days = price * sales30days;
307 }
308 }
309
310 competitors.push({
311 name: competitorName,
312 price: price,
313 sales30days: sales30days,
314 revenue30days: revenue30days,
315 sku: sku
316 });
317
318 console.log(`Div ${index}: Added competitor - ${competitorName}, Price: ${price}, Sales: ${sales30days}, Revenue: ${revenue30days}, SKU: ${sku}`);
319 } catch (error) {
320 console.error(`Div ${index}: Error extracting competitor data:`, error);
321 }
322 });
323
324 console.log(`Found ${competitors.length} competitors on product page`);
325
326 // If we found competitors on the page, return them
327 if (competitors.length > 0) {
328 console.log(`Total competitors extracted from page: ${competitors.length}`);
329
330 // Sort competitors by revenue (highest first)
331 competitors.sort((a, b) => {
332 const revenueA = a.revenue30days || 0;
333 const revenueB = b.revenue30days || 0;
334 return revenueB - revenueA;
335 });
336
337 return competitors;
338 }
339
340 console.log('No competitors found on page');
341 return null;
342
343 } catch (error) {
344 console.error('Error in extractCompetitorData:', error);
345 return null;
346 }
347 }
348
349 // Extract summary data from MP Stats widget
350 async function extractMPStatsData() {
351 console.log('Extracting MP Stats summary data...');
352
353 const widget = document.querySelector('.mps-sidebar');
354 if (!widget) {
355 console.error('MP Stats widget not found');
356 return null;
357 }
358
359 // Extract summary data
360 const revenueText = widget.textContent.match(/Выручка за 30 суток\s*([\d\s]+)/);
361 const salesText = widget.textContent.match(/Продаж за 30 суток\s*([\d\s]+)/);
362 const currentStockText = widget.textContent.match(/Текущий остаток\s*([\d\s]+)/);
363
364 if (revenueText && salesText) {
365 const revenue = parseInt(revenueText[1].replace(/\s/g, ''));
366 const sales = parseInt(salesText[1].replace(/\s/g, ''));
367
368 // Get minimum price from webPrice widget
369 let avgPrice = revenue / sales; // fallback
370 const webPriceWidget = document.querySelector('[data-widget="webPrice"]');
371 if (webPriceWidget) {
372 const priceSpans = webPriceWidget.querySelectorAll('span');
373 const prices = [];
374 priceSpans.forEach(span => {
375 const text = span.textContent.trim();
376 // Look for price pattern (digits with optional spaces and ₽)
377 const match = text.match(/^(\d[\d\s]*)\s*₽$/);
378 if (match) {
379 const priceText = match[1].replace(/\s/g, '');
380 const parsedPrice = parseInt(priceText);
381 if (!isNaN(parsedPrice) && parsedPrice > 100 && parsedPrice < 100000) {
382 prices.push(parsedPrice);
383 }
384 }
385 });
386 if (prices.length > 0) {
387 avgPrice = Math.min(...prices);
388 console.log('Found prices in webPrice:', prices, 'Using minimum:', avgPrice);
389 }
390 }
391
392 const currentStock = currentStockText ? parseInt(currentStockText[1].replace(/\s/g, '')) : 0;
393
394 // Extract daily data from chart
395 const dataPoints = await extractDailyDataFromChart(widget);
396
397 if (!dataPoints || dataPoints.length === 0) {
398 console.error('Failed to extract chart data');
399 return null;
400 }
401
402 // Extract stock data from second chart
403 const stockPoints = extractStockData();
404
405 // Calculate average daily sales from actual data
406 const totalSales = dataPoints.reduce((sum, d) => sum + d.sales, 0);
407 const avgDailySales = totalSales / dataPoints.length;
408
409 console.log('Extracted summary data:', {
410 revenue,
411 sales,
412 avgPrice,
413 currentStock,
414 avgDailySales,
415 daysOfData: dataPoints.length,
416 stockDataPoints: stockPoints ? stockPoints.length : 0
417 });
418
419 return {
420 revenue,
421 sales,
422 avgPrice,
423 currentStock,
424 avgDailySales,
425 dataPoints,
426 stockPoints
427 };
428 }
429
430 return null;
431 }
432
433 // AI-powered Bayesian price optimization
434 async function bayesianPriceOptimizationWithAI(historicalData, productData, competitorData) {
435 console.log('Running AI-powered Bayesian price optimization...');
436 console.log('Historical data:', historicalData);
437 console.log('Product data:', productData);
438 console.log('Competitor data:', competitorData);
439
440 try {
441 // Prepare daily sales data for AI
442 const dailySalesInfo = historicalData.dataPoints
443 .map(d => `День ${d.day}: ${d.sales} продаж по цене ${d.price}₽`)
444 .join('\n');
445
446 // Calculate unique prices used
447 const uniquePrices = [...new Set(historicalData.dataPoints.map(d => d.price))];
448 const priceChanges = uniquePrices.length;
449
450 console.log(`Historical data contains ${historicalData.dataPoints.length} days with ${priceChanges} unique prices:`, uniquePrices);
451
452 // Prepare stock data for AI if available
453 let stockInfo = '';
454 if (historicalData.stockPoints && historicalData.stockPoints.length > 0) {
455 stockInfo = '\n\nДАННЫЕ ОБ ОСТАТКАХ ПО ДНЯМ:\n' +
456 historicalData.stockPoints
457 .map(d => `День ${d.day}: ${d.stock} шт на складе`)
458 .join('\n');
459 }
460
461 // Prepare competitor data for AI if available
462 let competitorInfo = '';
463 if (competitorData && competitorData.length > 0) {
464 competitorInfo = '\n\nДАННЫЕ КОНКУРЕНТОВ (товары с тем же первым словом в названии):\n';
465 competitorData.forEach((comp, index) => {
466 competitorInfo += `${index + 1}. Цена: ${comp.price} ₽`;
467 if (comp.sales30days) {
468 competitorInfo += `, Продажи за 30 дней: ${comp.sales30days} шт`;
469 const avgDailySales = (comp.sales30days / 30).toFixed(1);
470 competitorInfo += ` (${avgDailySales} шт/день)`;
471 }
472 if (comp.revenue30days) {
473 competitorInfo += `, Выручка за 30 дней: ${comp.revenue30days} ₽`;
474 }
475 if (comp.sku) {
476 competitorInfo += `, SKU: ${comp.sku}`;
477 }
478 competitorInfo += '\n';
479 });
480
481 // Calculate competitor statistics
482 const prices = competitorData.map(c => c.price);
483 const minCompPrice = Math.min(...prices);
484 const maxCompPrice = Math.max(...prices);
485 const avgCompPrice = Math.round(prices.reduce((a, b) => a + b, 0) / prices.length);
486
487 competitorInfo += '\nСтатистика конкурентов:\n';
488 competitorInfo += `- Минимальная цена: ${minCompPrice} ₽\n`;
489 competitorInfo += `- Максимальная цена: ${maxCompPrice} ₽\n`;
490 competitorInfo += `- Средняя цена: ${avgCompPrice} ₽\n`;
491 competitorInfo += `- Количество конкурентов: ${competitorData.length}\n`;
492 }
493
494 const commissionPercent = Math.round(productData.commission * 100);
495
496 const aiPrompt = `Ты эксперт по ценообразованию на маркетплейсах. Проанализируй данные товара и предложи оптимальную цену для МАКСИМИЗАЦИИ ОБЩЕЙ ПРИБЫЛИ В ДЕНЬ.
497
498ДАННЫЕ ТОВАРА:
499- Выручка за 30 дней: ${historicalData.revenue} ₽
500- Продаж за 30 дней: ${historicalData.sales} шт
501- Текущая цена: ${Math.round(historicalData.avgPrice)} ₽
502- Текущий остаток: ${historicalData.currentStock} ед
503- Среднее кол-во продаж в день: ${historicalData.avgDailySales.toFixed(1)} шт
504- Себестоимость: ${productData.cost} ₽
505- Комиссия маркетплейса: ${commissionPercent}% от цены
506- Стоимость доставки: ${productData.delivery} ₽ за единицу
507
508ДЕТАЛЬНЫЕ ДАННЫЕ ПО ДНЯМ (последние 30 дней):
509${dailySalesInfo}${stockInfo}${competitorInfo}
510
511КРИТИЧЕСКИ ВАЖНО - ФОРМУЛА РАСЧЁТА ПРИБЫЛИ:
512Чистая цена = Цена - (Цена × ${productData.commission}) - ${productData.delivery}
513Прибыль с единицы = Чистая цена - Себестоимость
514ОБЩАЯ ПРИБЫЛЬ В ДЕНЬ = Прибыль с единицы × Количество продаж в день
515
516ПРИМЕР РАСЧЁТА:
517При цене 504₽:
518- Чистая цена = 504 - (504×${productData.commission}) - ${productData.delivery} = 504 - ${Math.round(504 * productData.commission)} - ${productData.delivery} = ${Math.round(504 - 504 * productData.commission - productData.delivery)}₽
519- Прибыль с единицы = ${Math.round(504 - 504 * productData.commission - productData.delivery - productData.cost)}₽
520- При продажах ${historicalData.avgDailySales.toFixed(1)} шт/день
521- ОБЩАЯ прибыль = ${Math.round(504 - 504 * productData.commission - productData.delivery - productData.cost)}₽ × ${historicalData.avgDailySales.toFixed(1)} = ${Math.round((504 - 504 * productData.commission - productData.delivery - productData.cost) * historicalData.avgDailySales)}₽/день
522
523ТВОЯ ЗАДАЧА: Найти цену, которая максимизирует ОБЩУЮ ПРИБЫЛЬ В ДЕНЬ, а не выручку или количество продаж!
524
525Проанализируй используя продвинутые методы:
5261. Адаптивную байесовскую модель для определения эластичности спроса
5272. Томпсоновское сэмплирование для учёта неопределённости
5283. Гауссовские процессы для моделирования зависимости продаж от цены
5294. Multi-Armed Bandit подход для балансировки exploration/exploitation
5305. Учти динамику продаж по дням - если видишь тренд снижения или роста
5316. ВАЖНО: Проанализируй корреляцию между остатками и продажами - низкие остатки могут ограничивать продажи
5327. ВАЖНО: Учти конкурентное окружение - цены и продажи конкурентов помогут определить оптимальную позицию на рынке
5338. КРИТИЧЕСКИ ВАЖНО: Из-за высокой себестоимости (${productData.cost}₽) и фиксированных расходов (комиссия ${commissionPercent}% + доставка ${productData.delivery}₽), снижение цены очень сильно снижает прибыль с единицы. Рост продаж должен компенсировать это снижение!
534
535Верни JSON с рекомендациями:
536- optimalPrice: оптимальная цена для максимизации прибыли
537- demandElasticity: коэффициент эластичности спроса (обычно от -0.5 до -3.0)
538- minPrice: минимальная рекомендуемая цена (должна покрывать все расходы)
539- maxPrice: максимальная рекомендуемая цена
540- confidence: уровень уверенности (0-1)
541- reasoning: подробное объяснение логики (2-3 предложения, почему именно эта цена максимизирует ОБЩУЮ прибыль, с конкретными цифрами прибыли)
542- explorationPrices: массив из 3 цен для дополнительного тестирования (exploration)`;
543
544 const aiResponse = await RM.aiCall(aiPrompt, {
545 type: 'json_schema',
546 json_schema: {
547 name: 'price_optimization',
548 schema: {
549 type: 'object',
550 properties: {
551 optimalPrice: {
552 type: 'number',
553 description: 'Оптимальная цена для максимизации прибыли'
554 },
555 demandElasticity: {
556 type: 'number',
557 description: 'Коэффициент эластичности спроса (обычно от -0.5 до -3.0)'
558 },
559 minPrice: {
560 type: 'number',
561 description: 'Минимальная рекомендуемая цена'
562 },
563 maxPrice: {
564 type: 'number',
565 description: 'Максимальная рекомендуемая цена'
566 },
567 confidence: {
568 type: 'number',
569 description: 'Уровень уверенности в рекомендации (0-1)'
570 },
571 reasoning: {
572 type: 'string',
573 description: 'Подробное объяснение логики расчёта (2-3 предложения)'
574 },
575 explorationPrices: {
576 type: 'array',
577 items: { type: 'number' },
578 description: 'Массив из 3 цен для дополнительного тестирования'
579 }
580 },
581 required: ['optimalPrice', 'demandElasticity', 'minPrice', 'maxPrice', 'confidence', 'reasoning', 'explorationPrices']
582 }
583 }
584 });
585
586 console.log('AI response:', aiResponse);
587
588 // Generate price range based on AI recommendations
589 const priceRange = [];
590 const step = (aiResponse.maxPrice - aiResponse.minPrice) / 20;
591
592 for (let price = aiResponse.minPrice; price <= aiResponse.maxPrice; price += step) {
593 priceRange.push(Math.round(price));
594 }
595
596 // Add exploration prices from AI
597 aiResponse.explorationPrices.forEach(p => {
598 if (!priceRange.includes(Math.round(p))) {
599 priceRange.push(Math.round(p));
600 }
601 });
602
603 priceRange.sort((a, b) => a - b);
604
605 // Рассчитываем показатели для каждой цены с учётом эластичности от ИИ
606 const results = priceRange.map(price => {
607 const priceRatio = price / historicalData.avgPrice;
608 const demandMultiplier = Math.pow(priceRatio, aiResponse.demandElasticity);
609 const estimatedSales = historicalData.avgDailySales * demandMultiplier;
610
611 // Расчёт с учётом комиссии 30% и доставки 90₽
612 const commission = price * 0.30;
613 const delivery = 90;
614 const netPrice = price - commission - delivery;
615 const profit = (netPrice - productData.cost) * estimatedSales;
616 const margin = ((netPrice - productData.cost) / price) * 100;
617
618 return {
619 price: Math.round(price * 10) / 10,
620 estimatedDailySales: Math.round(estimatedSales * 10) / 10,
621 estimatedDailyProfit: Math.round(profit * 10) / 10,
622 profitMargin: Math.round(margin * 10) / 10,
623 confidence: aiResponse.confidence,
624 commission: Math.round(commission * 10) / 10,
625 delivery: delivery,
626 netPrice: Math.round(netPrice * 10) / 10
627 };
628 });
629
630 // Find the optimal price recommended by AI
631 let optimalResult = results.find(r => r.price === aiResponse.optimalPrice);
632
633 // If exact price not found, calculate it
634 if (!optimalResult) {
635 const price = aiResponse.optimalPrice;
636 const priceRatio = price / historicalData.avgPrice;
637 const demandMultiplier = Math.pow(priceRatio, aiResponse.demandElasticity);
638 const estimatedSales = historicalData.avgDailySales * demandMultiplier;
639
640 const commission = price * 0.30;
641 const delivery = 90;
642 const netPrice = price - commission - delivery;
643 const profit = (netPrice - productData.cost) * estimatedSales;
644 const margin = ((netPrice - productData.cost) / price) * 100;
645
646 optimalResult = {
647 price: Math.round(price * 10) / 10,
648 estimatedDailySales: Math.round(estimatedSales * 10) / 10,
649 estimatedDailyProfit: Math.round(profit * 10) / 10,
650 profitMargin: Math.round(margin * 10) / 10,
651 confidence: aiResponse.confidence,
652 commission: Math.round(commission * 10) / 10,
653 delivery: delivery,
654 netPrice: Math.round(netPrice * 10) / 10
655 };
656 }
657
658 // Get alternatives from AI's exploration prices
659 const alternatives = [];
660 aiResponse.explorationPrices.forEach(explorePrice => {
661 if (explorePrice !== aiResponse.optimalPrice) {
662 let altResult = results.find(r => r.price === explorePrice);
663
664 if (!altResult) {
665 const priceRatio = explorePrice / historicalData.avgPrice;
666 const demandMultiplier = Math.pow(priceRatio, aiResponse.demandElasticity);
667 const estimatedSales = historicalData.avgDailySales * demandMultiplier;
668
669 const commission = explorePrice * 0.30;
670 const delivery = 90;
671 const netPrice = explorePrice - commission - delivery;
672 const profit = (netPrice - productData.cost) * estimatedSales;
673 const margin = ((netPrice - productData.cost) / explorePrice) * 100;
674
675 altResult = {
676 price: Math.round(explorePrice * 10) / 10,
677 estimatedDailySales: Math.round(estimatedSales * 10) / 10,
678 estimatedDailyProfit: Math.round(profit * 10) / 10,
679 profitMargin: Math.round(margin * 10) / 10,
680 confidence: aiResponse.confidence,
681 commission: Math.round(commission * 10) / 10,
682 delivery: delivery,
683 netPrice: Math.round(netPrice * 10) / 10
684 };
685 }
686
687 alternatives.push(altResult);
688 }
689 });
690
691 return {
692 optimal: optimalResult,
693 alternatives: alternatives,
694 currentPrice: Math.round(historicalData.avgPrice),
695 productData: productData,
696 allResults: results,
697 aiReasoning: aiResponse.reasoning,
698 aiConfidence: aiResponse.confidence,
699 aiDemandElasticity: aiResponse.demandElasticity,
700 explorationPrices: aiResponse.explorationPrices,
701 competitorData: competitorData,
702 historicalData: historicalData
703 };
704
705 } catch (error) {
706 console.error('AI analysis failed:', error);
707 throw error;
708 }
709 }
710
711 // Create and inject the analysis widget
712 function createAnalysisWidget() {
713 console.log('OZON Price Optimizer initialized');
714
715 // Check if widget already exists
716 if (document.getElementById('bayesian-price-optimizer')) {
717 console.log('Widget already exists');
718 return;
719 }
720
721 const widget = document.createElement('div');
722 widget.id = 'bayesian-price-optimizer';
723 widget.innerHTML = `
724 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
725 color: white;
726 padding: 15px;
727 border-radius: 12px;
728 margin: 15px 0;
729 box-shadow: 0 4px 15px rgba(0,0,0,0.2);
730 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
731 max-width: 40%;
732 box-sizing: border-box;">
733 <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
734 <h3 style="margin: 0; font-size: 16px; font-weight: 600;">
735 🎯 Оптимизация цены (AI + Bayesian)
736 </h3>
737 <button id="analyze-price-btn" style="background: white;
738 color: #667eea;
739 border: none;
740 padding: 8px 16px;
741 border-radius: 8px;
742 cursor: pointer;
743 font-weight: 600;
744 font-size: 13px;
745 transition: all 0.3s;
746 flex-shrink: 0;">
747 Анализировать
748 </button>
749 </div>
750 <div id="analysis-results" style="display: none; margin-top: 15px;">
751 <div style="background: rgba(255,255,255,0.15);
752 padding: 12px;
753 border-radius: 8px;
754 backdrop-filter: blur(10px);
755 max-width: 100%;
756 overflow: hidden;
757 box-sizing: border-box;">
758 <div id="results-content" style="max-width: 100%; overflow-wrap: break-word; word-wrap: break-word;"></div>
759 </div>
760 </div>
761 <div id="loading-indicator" style="display: none; text-align: center; padding: 20px;">
762 <div style="display: inline-block; width: 30px; height: 30px; border: 3px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 1s linear infinite;"></div>
763 <p style="margin-top: 10px; font-size: 13px;">Анализируем данные с помощью AI...</p>
764 </div>
765 </div>
766 `;
767
768 // Add CSS animation
769 const style = document.createElement('style');
770 style.textContent = `
771 @keyframes spin {
772 to { transform: rotate(360deg); }
773 }
774 #analyze-price-btn:hover {
775 transform: translateY(-2px);
776 box-shadow: 0 4px 12px rgba(0,0,0,0.15);
777 }
778 #bayesian-price-optimizer * {
779 box-sizing: border-box;
780 }
781 `;
782 document.head.appendChild(style);
783
784 // Find MP Stats widget and insert our widget after it
785 const mpsWidget = document.querySelector('.mps-sidebar');
786 if (mpsWidget) {
787 mpsWidget.parentElement.insertBefore(widget, mpsWidget.nextSibling);
788 console.log('Widget inserted after MP Stats');
789 } else {
790 document.body.appendChild(widget);
791 console.log('Widget inserted at body end');
792 }
793
794 // Add click handler
795 const analyzeBtn = document.getElementById('analyze-price-btn');
796 analyzeBtn.addEventListener('click', performAnalysis);
797 }
798
799 // Perform the price analysis
800 async function performAnalysis() {
801 console.log('Starting price analysis...');
802
803 const loadingIndicator = document.getElementById('loading-indicator');
804 const resultsDiv = document.getElementById('analysis-results');
805 const resultsContent = document.getElementById('results-content');
806
807 loadingIndicator.style.display = 'block';
808 resultsDiv.style.display = 'none';
809
810 try {
811 // Get product ID
812 const productId = getProductId();
813 console.log('Product ID:', productId);
814
815 if (!productId) {
816 throw new Error('Не удалось определить ID товара');
817 }
818
819 // Get product data using short SKU
820 const shortSku = getShortSku(productId);
821 console.log('Short SKU:', shortSku);
822
823 if (!shortSku) {
824 throw new Error('Данные для этого товара не найдены');
825 }
826
827 const productData = PRODUCT_DATA[shortSku];
828 console.log('Product data:', productData);
829
830 if (!productData) {
831 throw new Error('Данные для этого товара не найдены');
832 }
833
834 // Extract MP Stats data
835 const mpStatsData = await extractMPStatsData();
836 if (!mpStatsData) {
837 throw new Error('Не удалось извлечь данные из MP Stats');
838 }
839
840 // Extract competitor data
841 console.log('About to call extractCompetitorData...');
842 const competitorData = await extractCompetitorData();
843 console.log('Competitor data:', competitorData);
844
845 // Run Bayesian optimization with AI
846 const optimization = await bayesianPriceOptimizationWithAI(mpStatsData, productData, competitorData);
847
848 if (!optimization) {
849 throw new Error('Ошибка при оптимизации цены');
850 }
851
852 // Display results
853 displayResults(optimization);
854
855 } catch (error) {
856 console.error('Analysis error:', error);
857 resultsContent.innerHTML = `
858 <div style="color: #fee; padding: 10px; text-align: center;">
859 <strong>⚠️ Ошибка:</strong><br>
860 ${error.message}
861 </div>
862 `;
863 resultsDiv.style.display = 'block';
864 } finally {
865 loadingIndicator.style.display = 'none';
866 }
867 }
868
869 // Display optimization results
870 function displayResults(optimization) {
871 const resultsDiv = document.getElementById('analysis-results');
872 const resultsContent = document.getElementById('results-content');
873
874 const { optimal, alternatives, currentPrice, productData, allResults, aiReasoning, competitorData, historicalData } = optimization;
875
876 // Calculate current metrics using ACTUAL historical data
877 const currentDailySales = historicalData.avgDailySales; // Use actual historical sales
878 const currentCommission = currentPrice * productData.commission;
879 const currentDelivery = productData.delivery;
880 const currentNetPrice = currentPrice - currentCommission - currentDelivery;
881 const currentDailyProfit = Math.round((currentNetPrice - productData.cost) * currentDailySales);
882 const currentDailyRevenue = Math.round(currentPrice * currentDailySales);
883 const currentMargin = Math.round(((currentNetPrice - productData.cost) / currentPrice) * 100 * 10) / 10;
884 const currentProfitPerUnit = Math.round(currentNetPrice - productData.cost);
885
886 // Calculate optimal revenue
887 const optimalDailyRevenue = Math.round(optimal.price * optimal.estimatedDailySales);
888 const optimalProfitPerUnit = Math.round(optimal.netPrice - productData.cost);
889
890 // Calculate changes in percentages
891 const priceChange = Math.round(((optimal.price - currentPrice) / currentPrice) * 100);
892 const priceDiff = optimal.price - currentPrice;
893 const profitChange = currentDailyProfit > 0 ? Math.round(((optimal.estimatedDailyProfit - currentDailyProfit) / currentDailyProfit) * 100) : 0;
894 const profitDiff = optimal.estimatedDailyProfit - currentDailyProfit;
895 const revenueChange = currentDailyRevenue > 0 ? Math.round(((optimalDailyRevenue - currentDailyRevenue) / currentDailyRevenue) * 100) : 0;
896 const revenueDiff = optimalDailyRevenue - currentDailyRevenue;
897 const salesChange = currentDailySales > 0 ? Math.round(((optimal.estimatedDailySales - currentDailySales) / currentDailySales) * 100) : 0;
898 const salesDiff = Math.round((optimal.estimatedDailySales - currentDailySales) * 10) / 10;
899 const marginChange = Math.round((optimal.profitMargin - currentMargin) * 10) / 10;
900 const profitPerUnitChange = currentProfitPerUnit > 0 ? Math.round(((optimalProfitPerUnit - currentProfitPerUnit) / currentProfitPerUnit) * 100) : 0;
901 const profitPerUnitDiff = optimalProfitPerUnit - currentProfitPerUnit;
902
903 let html = `
904 <div style="background: rgba(16, 185, 129, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #10b981;">
905 <div style="font-size: 12px; font-weight: 600; margin-bottom: 8px; opacity: 0.9;">🤖 Рекомендация AI:</div>
906 <div style="font-size: 13px; line-height: 1.5; opacity: 0.95; word-wrap: break-word; white-space: normal; overflow-wrap: break-word;">${aiReasoning}</div>
907 <div style="font-size: 11px; margin-top: 8px; opacity: 0.8; border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px;">
908 📊 Эластичность спроса: <strong>${historicalData.dataPoints.length} дней данных, ${[...new Set(optimization.historicalData.dataPoints.map(d => d.price))].length} уникальных цен</strong> → коэффициент <strong>${Math.abs(optimization.aiDemandElasticity).toFixed(2)}</strong>
909 </div>
910 </div>
911 `;
912
913 // Add competitor info if available
914 if (competitorData && competitorData.length > 0) {
915 html += `
916 <div style="background: rgba(59, 130, 246, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #3b82f6;">
917 <div style="font-size: 12px; font-weight: 600; margin-bottom: 4px; opacity: 0.9;">
918 🏪 Анализ конкурентов (${competitorData.length} товаров):
919 </div>
920 `;
921
922 // Show ALL competitors with links, sales, and revenue
923 competitorData.forEach((comp, index) => {
924 const competitorUrl = comp.sku ? `https://www.ozon.ru/product/${comp.sku}/` : '#';
925 const salesInfo = comp.sales30days ? ` • ${comp.sales30days} шт/30д (${(comp.sales30days / 30).toFixed(1)} шт/день)` : '';
926 const revenueInfo = comp.revenue30days ? ` • ${comp.revenue30days.toLocaleString('ru-RU')} ₽/30д` : '';
927
928 html += `
929 <div style="font-size: 12px; padding: 6px 0; opacity: 0.9;">
930 ${index + 1}. <a href="${competitorUrl}" target="_blank" style="color: white; text-decoration: underline;">${comp.price} ₽</a>${salesInfo}${revenueInfo}
931 </div>
932 `;
933 });
934
935 html += '</div>';
936 } else {
937 html += `
938 <div style="background: rgba(156, 163, 175, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #9ca3af;">
939 <div style="font-size: 12px; font-weight: 600; margin-bottom: 4px; opacity: 0.9;">
940 🏪 Анализ конкурентов:
941 </div>
942 <div style="font-size: 11px; opacity: 0.8;">
943 Конкуренты не найдены на странице товара
944 </div>
945 </div>
946 `;
947 }
948
949 html += `
950 <div style="background: #10b981; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; display: inline-block; margin-bottom: 12px;">
951 ✨ ОПТИМАЛЬНАЯ ЦЕНА
952 </div>
953
954 <table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
955 <thead>
956 <tr style="border-bottom: 2px solid rgba(255,255,255,0.3);">
957 <th style="text-align: left; padding: 8px; font-size: 13px; opacity: 0.9;">Показатель</th>
958 <th style="text-align: right; padding: 8px; font-size: 13px; opacity: 0.9;">Текущая</th>
959 <th style="text-align: right; padding: 8px; font-size: 13px; opacity: 0.9;">Рекомендуемая</th>
960 <th style="text-align: right; padding: 8px; font-size: 13px; opacity: 0.9;">Разница</th>
961 </tr>
962 </thead>
963 <tbody>
964 <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
965 <td style="padding: 10px 8px; font-size: 13px;">Цена</td>
966 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentPrice} ₽</td>
967 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
968 ${optimal.price} ₽
969 <span style="color: ${priceChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; margin-left: 5px; font-weight: 700;">
970 (${priceChange >= 0 ? '+' : ''}${priceChange}%)
971 </span>
972 </td>
973 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${priceDiff >= 0 ? '#10b981' : '#ef4444'};">
974 ${priceDiff >= 0 ? '+' : ''}${priceDiff} ₽
975 </td>
976 </tr>
977 <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
978 <td style="padding: 10px 8px; font-size: 13px;">Прибыль/день</td>
979 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentDailyProfit} ₽</td>
980 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
981 ${optimal.estimatedDailyProfit} ₽
982 <span style="color: ${profitChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; margin-left: 5px; font-weight: 700;">
983 (${profitChange >= 0 ? '+' : ''}${profitChange}%)
984 </span>
985 </td>
986 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${profitDiff >= 0 ? '#10b981' : '#ef4444'};">
987 ${profitDiff >= 0 ? '+' : ''}${profitDiff} ₽
988 </td>
989 </tr>
990 <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
991 <td style="padding: 10px 8px; font-size: 13px;">Выручка/день</td>
992 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentDailyRevenue} ₽</td>
993 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
994 ${optimalDailyRevenue} ₽
995 <span style="color: ${revenueChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; margin-left: 5px; font-weight: 700;">
996 (${revenueChange >= 0 ? '+' : ''}${revenueChange}%)
997 </span>
998 </td>
999 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${revenueDiff >= 0 ? '#10b981' : '#ef4444'};">
1000 ${revenueDiff >= 0 ? '+' : ''}${revenueDiff} ₽
1001 </td>
1002 </tr>
1003 <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
1004 <td style="padding: 10px 8px; font-size: 13px;">Продажи/день</td>
1005 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentDailySales} шт</td>
1006 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
1007 ${optimal.estimatedDailySales} шт
1008 <span style="color: ${salesChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; margin-left: 5px; font-weight: 700;">
1009 (${salesChange >= 0 ? '+' : ''}${salesChange}%)
1010 </span>
1011 </td>
1012 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${salesDiff >= 0 ? '#10b981' : '#ef4444'};">
1013 ${salesDiff >= 0 ? '+' : ''}${salesDiff} шт
1014 </td>
1015 </tr>
1016 <tr style="border-bottom: 1px solid rgba(255,255,255,0.1);">
1017 <td style="padding: 10px 8px; font-size: 13px;">Маржа %</td>
1018 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentMargin}%</td>
1019 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
1020 ${optimal.profitMargin}%
1021 <span style="color: ${marginChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; margin-left: 5px; font-weight: 700;">
1022 (${marginChange >= 0 ? '+' : ''}${marginChange}%)
1023 </span>
1024 </td>
1025 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${marginChange >= 0 ? '#10b981' : '#ef4444'};">
1026 ${marginChange >= 0 ? '+' : ''}${marginChange}%
1027 </td>
1028 </tr>
1029 <tr>
1030 <td style="padding: 10px 8px; font-size: 13px;">Прибыль/шт</td>
1031 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">${currentProfitPerUnit} ₽</td>
1032 <td style="padding: 10px 8px; text-align: right; font-weight: 600;">
1033 ${optimalProfitPerUnit} ₽
1034 <span style="color: ${profitPerUnitChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; font-weight: 700; margin-left: 5px;">
1035 (${profitPerUnitChange >= 0 ? '+' : ''}${profitPerUnitChange}%)
1036 </span>
1037 </td>
1038 <td style="padding: 10px 8px; text-align: right; font-weight: 600; color: ${profitPerUnitDiff >= 0 ? '#10b981' : '#ef4444'};">
1039 ${profitPerUnitDiff >= 0 ? '+' : ''}${profitPerUnitDiff} ₽
1040 </td>
1041 </tr>
1042 </tbody>
1043 </table>
1044 `;
1045
1046 if (alternatives && alternatives.length > 0) {
1047 html += `
1048 <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); width: 100%; box-sizing: border-box;">
1049 <div style="font-size: 13px; margin-bottom: 10px; opacity: 0.9; font-weight: 600;">
1050 📊 Альтернативные варианты:
1051 </div>
1052 `;
1053
1054 alternatives.slice(0, 3).forEach((alt, index) => {
1055 const altProfitChange = currentDailyProfit > 0 ? Math.round(((alt.estimatedDailyProfit - currentDailyProfit) / currentDailyProfit) * 100) : 0;
1056 html += `
1057 <div style="font-size: 14px; padding: 10px; opacity: 0.95; border-radius: 6px; margin-bottom: 8px; background: rgba(255,255,255,0.1);">
1058 <strong>${index + 2}. ${alt.price} ₽</strong> → <strong>${alt.estimatedDailyProfit} ₽/день</strong>
1059 <span style="color: ${altProfitChange >= 0 ? '#10b981' : '#ef4444'}; font-size: 13px; font-weight: 700; margin-left: 5px;">
1060 (${altProfitChange >= 0 ? '+' : ''}${altProfitChange}%)
1061 </span>
1062 <span style="opacity: 0.8; margin-left: 5px;">(${alt.estimatedDailySales} шт)</span>
1063 </div>
1064 `;
1065 });
1066
1067 html += '</div>';
1068 }
1069
1070 html += `
1071 <div style="margin-top: 15px; padding: 10px; background: rgba(255,255,255,0.1); border-radius: 6px; font-size: 11px; opacity: 0.8;">
1072 💡 Анализ использует AI + байесовский подход с учетом всех ${allResults.length} вариантов цен.
1073 Уверенность: ${Math.round(optimal.confidence * 100)}%
1074 </div>
1075 `;
1076
1077 resultsContent.innerHTML = html;
1078 resultsDiv.style.display = 'block';
1079
1080 // Create chart after displaying results
1081 setTimeout(() => createPriceChart(optimization), 100);
1082 }
1083
1084 // Create price analysis chart
1085 function createPriceChart(optimization) {
1086 // Remove old chart if exists
1087 const oldChart = document.getElementById('price-analysis-chart');
1088 if (oldChart) {
1089 oldChart.parentElement.remove();
1090 }
1091
1092 const canvas = document.createElement('canvas');
1093 canvas.id = 'price-analysis-chart';
1094 canvas.style.cssText = 'width: 100% !important; height: 300px !important;';
1095
1096 const chartContainer = document.createElement('div');
1097 chartContainer.style.cssText = 'margin-top: 15px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px; width: 100%; box-sizing: border-box;';
1098 chartContainer.innerHTML = `
1099 <div style="font-size: 13px; margin-bottom: 10px; opacity: 0.9; font-weight: 600;">
1100 📈 График зависимости показателей от цены
1101 </div>
1102 `;
1103 chartContainer.appendChild(canvas);
1104
1105 const resultsContent = document.getElementById('results-content');
1106 resultsContent.appendChild(chartContainer);
1107
1108 // Prepare data for chart
1109 const sortedResults = [...optimization.allResults].sort((a, b) => a.price - b.price);
1110 const prices = sortedResults.map(r => r.price);
1111 const sales = sortedResults.map(r => r.estimatedDailySales);
1112 const revenue = sortedResults.map(r => Math.round(r.price * r.estimatedDailySales));
1113 const profit = sortedResults.map(r => r.estimatedDailyProfit);
1114
1115 // Create chart using Chart.js
1116 const ctx = canvas.getContext('2d');
1117 window.priceAnalysisChart = new Chart(ctx, {
1118 type: 'line',
1119 data: {
1120 labels: prices,
1121 datasets: [
1122 {
1123 label: 'Продажи (шт/день)',
1124 data: sales,
1125 borderColor: '#10b981',
1126 backgroundColor: 'rgba(16, 185, 129, 0.1)',
1127 yAxisID: 'y',
1128 tension: 0.4
1129 },
1130 {
1131 label: 'Выручка (₽/день)',
1132 data: revenue,
1133 borderColor: '#f59e0b',
1134 backgroundColor: 'rgba(245, 158, 11, 0.1)',
1135 yAxisID: 'y1',
1136 tension: 0.4
1137 },
1138 {
1139 label: 'Прибыль (₽/день)',
1140 data: profit,
1141 borderColor: '#ef4444',
1142 backgroundColor: 'rgba(239, 68, 68, 0.1)',
1143 yAxisID: 'y1',
1144 tension: 0.4,
1145 borderWidth: 3
1146 }
1147 ]
1148 },
1149 options: {
1150 responsive: true,
1151 maintainAspectRatio: true,
1152 aspectRatio: 1.5,
1153 interaction: {
1154 mode: 'index',
1155 intersect: false,
1156 },
1157 plugins: {
1158 legend: {
1159 position: 'top',
1160 labels: {
1161 usePointStyle: true,
1162 padding: 15,
1163 font: {
1164 size: 11
1165 },
1166 color: 'white'
1167 }
1168 },
1169 tooltip: {
1170 backgroundColor: 'rgba(0, 0, 0, 0.8)',
1171 padding: 12,
1172 titleFont: {
1173 size: 13
1174 },
1175 bodyFont: {
1176 size: 12
1177 },
1178 callbacks: {
1179 title: function(context) {
1180 return 'Цена: ' + context[0].label + ' ₽';
1181 },
1182 label: function(context) {
1183 let label = context.dataset.label || '';
1184 if (label) {
1185 label += ': ';
1186 }
1187 if (context.parsed.y !== null) {
1188 if (label.includes('шт')) {
1189 label += context.parsed.y.toFixed(1);
1190 } else {
1191 label += Math.round(context.parsed.y);
1192 }
1193 }
1194 return label;
1195 }
1196 }
1197 }
1198 },
1199 scales: {
1200 x: {
1201 title: {
1202 display: true,
1203 text: 'Цена (₽)',
1204 font: {
1205 size: 12,
1206 weight: 'bold'
1207 },
1208 color: 'white'
1209 },
1210 ticks: {
1211 maxTicksLimit: 10,
1212 font: {
1213 size: 10
1214 },
1215 color: 'rgba(255, 255, 255, 0.8)'
1216 },
1217 grid: {
1218 color: 'rgba(255, 255, 255, 0.1)'
1219 }
1220 },
1221 y: {
1222 type: 'linear',
1223 display: true,
1224 position: 'left',
1225 title: {
1226 display: true,
1227 text: 'Продажи (шт/день)',
1228 color: '#10b981',
1229 font: {
1230 size: 11
1231 }
1232 },
1233 ticks: {
1234 font: {
1235 size: 10
1236 },
1237 color: 'rgba(255, 255, 255, 0.8)'
1238 },
1239 grid: {
1240 color: 'rgba(255, 255, 255, 0.1)'
1241 }
1242 },
1243 y1: {
1244 type: 'linear',
1245 display: true,
1246 position: 'right',
1247 title: {
1248 display: true,
1249 text: 'Выручка/Прибыль (₽/день)',
1250 color: '#f59e0b',
1251 font: {
1252 size: 11
1253 }
1254 },
1255 grid: {
1256 drawOnChartArea: false,
1257 },
1258 ticks: {
1259 font: {
1260 size: 10
1261 },
1262 color: 'rgba(255, 255, 255, 0.8)'
1263 }
1264 }
1265 }
1266 }
1267 });
1268
1269 console.log('График создан успешно');
1270 }
1271
1272 // Wait for MP Stats widget to load
1273 function waitForMPStats() {
1274 const checkInterval = setInterval(() => {
1275 const mpsWidget = document.querySelector('.mps-sidebar');
1276 const chartSvg = mpsWidget ? mpsWidget.querySelector('.vue-apexcharts svg') : null;
1277 const bars = chartSvg ? chartSvg.querySelectorAll('.apexcharts-bar-area') : null;
1278
1279 if (mpsWidget && chartSvg && bars && bars.length > 0) {
1280 console.log('MP Stats widget and chart found, creating analysis widget...');
1281 clearInterval(checkInterval);
1282 setTimeout(createAnalysisWidget, 1000);
1283 }
1284 }, 1000);
1285
1286 // Stop checking after 30 seconds
1287 setTimeout(() => {
1288 clearInterval(checkInterval);
1289 console.log('Stopped waiting for MP Stats widget');
1290 }, 30000);
1291 }
1292
1293 // Initialize
1294 function init() {
1295 console.log('OZON Price Optimizer initialized');
1296
1297 // Check if we're on a product page
1298 const productId = getProductId();
1299 if (!productId) {
1300 console.log('Not a product page, skipping...');
1301 return;
1302 }
1303
1304 console.log('Product page detected, waiting for MP Stats widget...');
1305
1306 // Wait for page to load
1307 if (document.readyState === 'loading') {
1308 document.addEventListener('DOMContentLoaded', waitForMPStats);
1309 } else {
1310 waitForMPStats();
1311 }
1312 }
1313
1314 init();
1315})();