Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
Size
137.3 KB
Version
1.1.59
Created
Dec 8, 2025
Updated
about 1 month ago
1// ==UserScript==
2// @name Ozon AI Analyzer 5.0
3// @description Мощный AI-аналитик для выявления проблем с продажами, анализа показателей и рекомендаций по улучшению
4// @version 1.1.59
5// @match https://*.seller.ozon.ru/*
6// @icon https://st.ozone.ru/s3/seller-ui-static/icon/favicon32.png
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.xmlHttpRequest
10// ==/UserScript==
11(function() {
12
13 'use strict';
14
15
16 console.log('🚀 Ozon AI Аналитик Продаж запущен');
17
18
19 // Утилита для задержки
20
21 const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
22
23
24 // Парсинг числовых значений
25
26 function parseNumber(str) {
27
28 if (!str || str === '—' || str === '') return null;
29
30 // Извлекаем первое число ДО знака процента
31
32 const match = str.match(/^([\d\s,.]+)/);
33
34 if (!match) return null;
35
36 // Убираем пробелы (разделители тысяч), потом парсим
37
38 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
39
40 const num = parseFloat(cleaned);
41
42 return isNaN(num) ? null : num;
43
44 }
45
46
47 // Парсинг процентов
48
49 function parsePercent(str) {
50
51 if (!str || str === '—' || str === '') return null;
52
53 const match = str.match(/([+-]?\d+(?:\.\d+)?)\s*%/);
54
55 return match ? parseFloat(match[1]) : null;
56
57 }
58
59
60 // Парсинг цены (убираем пробелы между цифрами)
61
62 function parsePrice(str) {
63
64 if (!str || str === '—' || str === '') return null;
65
66 // Извлекаем первое число ДО знака процента
67
68 const match = str.match(/^([\d\s,.]+)/);
69
70 if (!match) return null;
71
72 // Убираем пробелы, затем все кроме цифр и точек
73
74 const cleaned = match[1].replace(/\s/g, '').replace(',', '.');
75
76 const num = parseFloat(cleaned);
77
78 return isNaN(num) ? null : num;
79
80 }
81
82
83 // Таблица с данными для расчета прибыли
84
85 const PRODUCT_COST_DATA = {
86
87
88 '72252': { cost: 158.4, commission: 0.30, delivery: 90 },
89 '71613': { cost: 108, commission: 0.30, delivery: 90 },
90 '73716': { cost: 126, commission: 0.30, delivery: 90 },
91 '80036': { cost: 103.2, commission: 0.30, delivery: 90 },
92 '73365': { cost: 166.8, commission: 0.30, delivery: 90 },
93 '74881': { cost: 135.6, commission: 0.30, delivery: 90 },
94 '73266': { cost: 708, commission: 0.30, delivery: 90 },
95 '73655': { cost: 219.6, commission: 0.30, delivery: 90 },
96 '75222': { cost: 103.2, commission: 0.30, delivery: 90 },
97 '73358': { cost: 163.2, commission: 0.30, delivery: 90 },
98 '73723': { cost: 116.4, commission: 0.30, delivery: 90 },
99 '74119': { cost: 110.4, commission: 0.30, delivery: 90 },
100 '72573': { cost: 146.4, commission: 0.30, delivery: 90 },
101 '72221': { cost: 90, commission: 0.30, delivery: 90 },
102 '75345': { cost: 111.6, commission: 0.30, delivery: 90 },
103 '73334': { cost: 104.4, commission: 0.30, delivery: 90 },
104 '72184': { cost: 177.6, commission: 0.30, delivery: 90 },
105 '75291': { cost: 100.8, commission: 0.30, delivery: 90 },
106 '73617': { cost: 163.2, commission: 0.30, delivery: 90 },
107 '73976': { cost: 170.4, commission: 0.30, delivery: 90 },
108 '75710': { cost: 172.8, commission: 0.30, delivery: 90 },
109 '76113': { cost: 115.2, commission: 0.30, delivery: 90 },
110 '75499': { cost: 96, commission: 0.30, delivery: 90 },
111 '71569': { cost: 117.6, commission: 0.30, delivery: 90 },
112 '72276': { cost: 286.8, commission: 0.30, delivery: 90 },
113 '75338': { cost: 90, commission: 0.30, delivery: 90 },
114 '75536': { cost: 87.6, commission: 0.30, delivery: 90 },
115 '76014': { cost: 135.6, commission: 0.30, delivery: 90 },
116 '73730': { cost: 121.2, commission: 0.30, delivery: 90 },
117 '75628': { cost: 124.8, commission: 0.30, delivery: 90 },
118 '74249': { cost: 312, commission: 0.30, delivery: 90 },
119 '75468': { cost: 138, commission: 0.30, delivery: 90 },
120 '73495': { cost: 120, commission: 0.30, delivery: 90 },
121 '74393': { cost: 126, commission: 0.30, delivery: 90 },
122 '74188': { cost: 174, commission: 0.30, delivery: 90 },
123 '73907': { cost: 155.376, commission: 0.30, delivery: 90 },
124 '73396': { cost: 112.8, commission: 0.30, delivery: 90 },
125 '71668': { cost: 96, commission: 0.30, delivery: 90 },
126 '73235': { cost: 129.6, commission: 0.30, delivery: 90 },
127 '75093': { cost: 96, commission: 0.30, delivery: 90 },
128 '73891': { cost: 100.8, commission: 0.30, delivery: 90 },
129 '75505': { cost: 91.2, commission: 0.30, delivery: 90 },
130 '71590': { cost: 100.8, commission: 0.30, delivery: 90 },
131 '73488': { cost: 150, commission: 0.30, delivery: 90 },
132 '75413': { cost: 128.4, commission: 0.30, delivery: 90 },
133 '76403': { cost: 352.8, commission: 0.30, delivery: 90 },
134 '74799': { cost: 162, commission: 0.30, delivery: 90 },
135 '75406': { cost: 117.6, commission: 0.30, delivery: 90 },
136 '75154': { cost: 123.6, commission: 0.30, delivery: 90 },
137 '75383': { cost: 82.8, commission: 0.30, delivery: 90 },
138 '80029': { cost: 67.416, commission: 0.30, delivery: 90 },
139 '76120': { cost: 264, commission: 0.30, delivery: 90 },
140 '72306': { cost: 186, commission: 0.30, delivery: 90 },
141 '75246': { cost: 88.8, commission: 0.30, delivery: 90 },
142 '73228': { cost: 133.476, commission: 0.30, delivery: 90 },
143 '73419': { cost: 165.6, commission: 0.30, delivery: 90 },
144 '74379': { cost: 175.2, commission: 0.30, delivery: 90 },
145 '83356': { cost: 229.2, commission: 0.30, delivery: 90 },
146 '75444': { cost: 123.6, commission: 0.30, delivery: 90 },
147 '79992': { cost: 127.2, commission: 0.30, delivery: 90 },
148 '73709': { cost: 218.4, commission: 0.30, delivery: 90 },
149 '73778': { cost: 144, commission: 0.30, delivery: 90 },
150 '72269': { cost: 194.4, commission: 0.30, delivery: 90 },
151 '73440': { cost: 118.8, commission: 0.30, delivery: 90 },
152 '74669': { cost: 192, commission: 0.30, delivery: 90 },
153 '77660': { cost: 115.2, commission: 0.30, delivery: 90 },
154 '77578': { cost: 249.6, commission: 0.30, delivery: 90 },
155 '71545': { cost: 124.8, commission: 0.30, delivery: 90 },
156 '75673': { cost: 97.2, commission: 0.30, delivery: 90 },
157 '76168': { cost: 80.4, commission: 0.30, delivery: 90 },
158 '75277': { cost: 217.2, commission: 0.30, delivery: 90 },
159 '75390': { cost: 108, commission: 0.30, delivery: 90 },
160 '74263': { cost: 160.8, commission: 0.30, delivery: 90 },
161 '74676': { cost: 176.4, commission: 0.30, delivery: 90 },
162 '75727': { cost: 121.2, commission: 0.30, delivery: 90 },
163 '74126': { cost: 111.6, commission: 0.30, delivery: 90 },
164 '74294': { cost: 145.2, commission: 0.30, delivery: 90 },
165 '76069': { cost: 220.8, commission: 0.30, delivery: 90 },
166 '71361': { cost: 204, commission: 0.30, delivery: 90 },
167 '73501': { cost: 114, commission: 0.30, delivery: 90 },
168 '72238': { cost: 102, commission: 0.30, delivery: 90 },
169 '75482': { cost: 218.4, commission: 0.30, delivery: 90 },
170 '76489': { cost: 216, commission: 0.30, delivery: 90 },
171 '76076': { cost: 136.8, commission: 0.30, delivery: 90 },
172 '75437': { cost: 69.6, commission: 0.30, delivery: 90 },
173 '75352': { cost: 72, commission: 0.30, delivery: 90 },
174 '75550': { cost: 112.8, commission: 0.30, delivery: 90 },
175 '75529': { cost: 114, commission: 0.30, delivery: 90 },
176 '76021': { cost: 243.6, commission: 0.30, delivery: 90 },
177 '73969': { cost: 91.2, commission: 0.30, delivery: 90 },
178 '73242': { cost: 133.488, commission: 0.30, delivery: 90 },
179 '80111': { cost: 58.8, commission: 0.30, delivery: 90 },
180 '73693': { cost: 159.6, commission: 0.30, delivery: 90 },
181 '75703': { cost: 208.8, commission: 0.30, delivery: 90 },
182 '74980': { cost: 158.4, commission: 0.30, delivery: 90 },
183 '76380': { cost: 145.2, commission: 0.30, delivery: 90 },
184 '77677': { cost: 129.948, commission: 0.30, delivery: 90 },
185 '75369': { cost: 103.2, commission: 0.30, delivery: 90 },
186 '74713': { cost: 180, commission: 0.30, delivery: 90 },
187 '75024': { cost: 114, commission: 0.30, delivery: 90 },
188 '77615': { cost: 168, commission: 0.30, delivery: 90 },
189 '73389': { cost: 136.8, commission: 0.30, delivery: 90 },
190 '74850': { cost: 169.2, commission: 0.30, delivery: 90 },
191 '75192': { cost: 180, commission: 0.30, delivery: 90 },
192 '73310': { cost: 140.4, commission: 0.30, delivery: 90 },
193 '73280': { cost: 81.6, commission: 0.30, delivery: 90 },
194 '75048': { cost: 122.4, commission: 0.30, delivery: 90 },
195 '74874': { cost: 140.4, commission: 0.30, delivery: 90 },
196 '71675': { cost: 105.6, commission: 0.30, delivery: 90 },
197 '74225': { cost: 153.6, commission: 0.30, delivery: 90 },
198 '74768': { cost: 117.6, commission: 0.30, delivery: 90 },
199 '73136': { cost: 163.2, commission: 0.30, delivery: 90 },
200 '74300': { cost: 134.4, commission: 0.30, delivery: 90 },
201 '76410': { cost: 328.8, commission: 0.30, delivery: 90 },
202 '74898': { cost: 139.2, commission: 0.30, delivery: 90 },
203 '73129': { cost: 159.6, commission: 0.30, delivery: 90 },
204 '75253': { cost: 117.6, commission: 0.30, delivery: 90 },
205 '75666': { cost: 92.4, commission: 0.30, delivery: 90 },
206 '73839': { cost: 112.8, commission: 0.30, delivery: 90 },
207 '75475': { cost: 115.2, commission: 0.30, delivery: 90 },
208 '76397': { cost: 154.8, commission: 0.30, delivery: 90 },
209 '76083': { cost: 103.2, commission: 0.30, delivery: 90 },
210 '72207': { cost: 123.6, commission: 0.30, delivery: 90 },
211 '76151': { cost: 340.8, commission: 0.30, delivery: 90 },
212 '74911': { cost: 127.2, commission: 0.30, delivery: 90 },
213 '74775': { cost: 141.6, commission: 0.30, delivery: 90 },
214 '74027': { cost: 182.4, commission: 0.30, delivery: 90 },
215 '72245': { cost: 94.8, commission: 0.30, delivery: 90 },
216 '71705': { cost: 112.8, commission: 0.30, delivery: 90 },
217 '75109': { cost: 124.8, commission: 0.30, delivery: 90 },
218 '75260': { cost: 144, commission: 0.30, delivery: 90 },
219 '74584': { cost: 141.6, commission: 0.30, delivery: 90 },
220 '74331': { cost: 128.4, commission: 0.30, delivery: 90 },
221 '75307': { cost: 224.4, commission: 0.30, delivery: 90 },
222 '72542': { cost: 104.4, commission: 0.30, delivery: 90 },
223 '75642': { cost: 144.54, commission: 0.30, delivery: 90 },
224 '75512': { cost: 88.8, commission: 0.30, delivery: 90 },
225 '70999': { cost: 164.4, commission: 0.30, delivery: 90 },
226 '76137': { cost: 103.788, commission: 0.30, delivery: 90 },
227 '74072': { cost: 148.8, commission: 0.30, delivery: 90 },
228 '73297': { cost: 85.2, commission: 0.30, delivery: 90 },
229 '76465': { cost: 301.452, commission: 0.30, delivery: 90 },
230 '71835': { cost: 73.2, commission: 0.30, delivery: 90 },
231 '74324': { cost: 129.6, commission: 0.30, delivery: 90 },
232 '71644': { cost: 132, commission: 0.30, delivery: 90 },
233 '75420': { cost: 106.8, commission: 0.30, delivery: 90 },
234 '74355': { cost: 182.4, commission: 0.30, delivery: 90 },
235 '71651': { cost: 174, commission: 0.30, delivery: 90 },
236 '74973': { cost: 144, commission: 0.30, delivery: 90 },
237 '73341': { cost: 130.8, commission: 0.30, delivery: 90 },
238 '75185': { cost: 157.2, commission: 0.30, delivery: 90 },
239 '74348': { cost: 132, commission: 0.30, delivery: 90 },
240 '75376': { cost: 69.6, commission: 0.30, delivery: 90 },
241 '74942': { cost: 159.6, commission: 0.30, delivery: 90 },
242 '77592': { cost: 110.4, commission: 0.30, delivery: 90 },
243 '74737': { cost: 186, commission: 0.30, delivery: 90 },
244 '76045': { cost: 235.2, commission: 0.30, delivery: 90 },
245 '74256': { cost: 186, commission: 0.30, delivery: 90 },
246 '75208': { cost: 200.4, commission: 0.30, delivery: 90 },
247 '76601': { cost: 313.2, commission: 0.30, delivery: 90 },
248 '75116': { cost: 346.8, commission: 0.30, delivery: 90 },
249 '73464': { cost: 258, commission: 0.30, delivery: 90 },
250 '74577': { cost: 134.4, commission: 0.30, delivery: 90 },
251 '73792': { cost: 120, commission: 0.30, delivery: 90 },
252 '74997': { cost: 159.6, commission: 0.30, delivery: 90 },
253 '75611': { cost: 150, commission: 0.30, delivery: 90 },
254 '74782': { cost: 145.2, commission: 0.30, delivery: 90 },
255 '75031': { cost: 87.6, commission: 0.30, delivery: 90 },
256 '74195': { cost: 171.6, commission: 0.30, delivery: 90 },
257 '75161': { cost: 96, commission: 0.30, delivery: 90 },
258 '74591': { cost: 132, commission: 0.30, delivery: 90 },
259 '20107': { cost: 283.752, commission: 0.30, delivery: 90 },
260 '74935': { cost: 331.2, commission: 0.30, delivery: 90 },
261 '75062': { cost: 99.6, commission: 0.30, delivery: 90 },
262 '74706': { cost: 130.8, commission: 0.30, delivery: 90 },
263 '75147': { cost: 470.4, commission: 0.30, delivery: 90 },
264 '74744': { cost: 194.4, commission: 0.30, delivery: 90 },
265 '80241': { cost: 163.056, commission: 0.30, delivery: 90 },
266 '75000': { cost: 171.6, commission: 0.30, delivery: 90 },
267 '74270': { cost: 112.8, commission: 0.30, delivery: 90 },
268 '76229': { cost: 364.8, commission: 0.30, delivery: 90 },
269 '75284': { cost: 132, commission: 0.30, delivery: 90 },
270 '76212': { cost: 344.4, commission: 0.30, delivery: 90 },
271 '20091': { cost: 283.752, commission: 0.30, delivery: 90 },
272 '74096': { cost: 129.6, commission: 0.30, delivery: 90 },
273 '76595': { cost: 274.8, commission: 0.30, delivery: 90 },
274 '77646': { cost: 240, commission: 0.30, delivery: 90 },
275 '76526': { cost: 283.2, commission: 0.30, delivery: 90 },
276 '77608': { cost: 208.8, commission: 0.30, delivery: 90 },
277 '76557': { cost: 309.6, commission: 0.30, delivery: 90 },
278 '74867': { cost: 67.2, commission: 0.30, delivery: 90 },
279 '76571': { cost: 310.8, commission: 0.30, delivery: 90 },
280 '74409': { cost: 116.4, commission: 0.30, delivery: 90 },
281 '75604': { cost: 776.4, commission: 0.30, delivery: 90 },
282 '74720': { cost: 152.4, commission: 0.30, delivery: 90 },
283 '73914': { cost: 120, commission: 0.30, delivery: 90 },
284 '71552': { cost: 104.4, commission: 0.30, delivery: 90 },
285 '80767': { cost: 62.4, commission: 0.30, delivery: 90 },
286 '80302': { cost: 163.056, commission: 0.30, delivery: 90 },
287 '74430': { cost: 94.8, commission: 0.30, delivery: 90 },
288 '72948': { cost: 102, commission: 0.30, delivery: 90 },
289 '74683': { cost: 116.4, commission: 0.30, delivery: 90 },
290 '74485': { cost: 93.6, commission: 0.30, delivery: 90 },
291 '73303': { cost: 116.4, commission: 0.30, delivery: 90 },
292 '70111': { cost: 166.8, commission: 0.30, delivery: 90 },
293 '74607': { cost: 96, commission: 0.30, delivery: 90 },
294 '71422': { cost: 94.8, commission: 0.30, delivery: 90 },
295 '70081': { cost: 121.2, commission: 0.30, delivery: 90 },
296 '75178': { cost: 321.6, commission: 0.30, delivery: 90 },
297 '72191': { cost: 100.8, commission: 0.30, delivery: 90 },
298 '80159': { cost: 58.8, commission: 0.30, delivery: 90 },
299 '71378': { cost: 134.4, commission: 0.30, delivery: 90 },
300 '74966': { cost: 129.6, commission: 0.30, delivery: 90 },
301 '72504': { cost: 111.6, commission: 0.30, delivery: 90 },
302 '75130': { cost: 253.2, commission: 0.36, delivery: 200 },
303 '74843': { cost: 123.6, commission: 0.36, delivery: 200 },
304 '74539': { cost: 188.4, commission: 0.36, delivery: 200 },
305 '75123': { cost: 252, commission: 0.36, delivery: 200 },
306 '75017': { cost: 547.2, commission: 0.36, delivery: 200 },
307 '76366': { cost: 106.8, commission: 0.36, delivery: 200 },
308 '74232': { cost: 136.8, commission: 0.36, delivery: 200 },
309 '73198': { cost: 228, commission: 0.36, delivery: 200 },
310 '74447': { cost: 88.8, commission: 0.36, delivery: 200 },
311 '74560': { cost: 217.2, commission: 0.36, delivery: 200 },
312 '75239': { cost: 176.4, commission: 0.36, delivery: 200 },
313 '74423': { cost: 92.4, commission: 0.36, delivery: 200 },
314 '73174': { cost: 231.6, commission: 0.36, delivery: 200 },
315 '75635': { cost: 1267, commission: 0.36, delivery: 200 },
316 '73204': { cost: 216, commission: 0.36, delivery: 200 },
317 '74515': { cost: 223.2, commission: 0.36, delivery: 200 },
318 '70494': { cost: 358.8, commission: 0.36, delivery: 200 },
319 '73150': { cost: 216, commission: 0.36, delivery: 200 },
320 '75543': { cost: 1537, commission: 0.36, delivery: 200 },
321 '73181': { cost: 234, commission: 0.36, delivery: 200 },
322 '74812': { cost: 366, commission: 0.36, delivery: 200 },
323 '74805': { cost: 421.2, commission: 0.36, delivery: 200 },
324 '74010': { cost: 112.8, commission: 0.36, delivery: 200 },
325 '73167': { cost: 98.4, commission: 0.36, delivery: 200 },
326 '74416': { cost: 91.2, commission: 0.36, delivery: 200 },
327 '75574': { cost: 1517, commission: 0.36, delivery: 200 },
328 '74546': { cost: 240, commission: 0.36, delivery: 200 },
329 '76199': { cost: 271.2, commission: 0.36, delivery: 200 },
330 '74829': { cost: 372, commission: 0.36, delivery: 200 },
331 '80173': { cost: 768, commission: 0.36, delivery: 200 },
332 '75567': { cost: 1497, commission: 0.36, delivery: 200 },
333 '73754': { cost: 392.4, commission: 0.36, delivery: 200 },
334 '76298': { cost: 637.2, commission: 0.36, delivery: 200 },
335 '74454': { cost: 88.8, commission: 0.36, delivery: 200 },
336 '76205': { cost: 273.6, commission: 0.36, delivery: 200 },
337 '76274': { cost: 662.4, commission: 0.36, delivery: 200 },
338 '76441': { cost: 124.8, commission: 0.36, delivery: 200 },
339 '76434': { cost: 145.2, commission: 0.36, delivery: 200 },
340 '80227': { cost: 768, commission: 0.36, delivery: 200 },
341 '76281': { cost: 685.2, commission: 0.36, delivery: 200 },
342 '76243': { cost: 276, commission: 0.36, delivery: 200 },
343 '76267': { cost: 267.6, commission: 0.36, delivery: 200 },
344 '76250': { cost: 264, commission: 0.36, delivery: 200 },
345 '74478': { cost: 90, commission: 0.36, delivery: 200 },
346 '72856': { cost: 51.6, commission: 0.36, delivery: 200 },
347 '72887': { cost: 55.2, commission: 0.36, delivery: 200 },
348 '74461': { cost: 88.8, commission: 0.36, delivery: 200 },
349 '72894': { cost: 55.2, commission: 0.36, delivery: 200 },
350 '72924': { cost: 54, commission: 0.36, delivery: 200 },
351 '72849': { cost: 52.8, commission: 0.36, delivery: 200 },
352 '80180': { cost: 768, commission: 0.36, delivery: 200 },
353 '72825': { cost: 61.2, commission: 0.36, delivery: 200 },
354 '70548': { cost: 106.8, commission: 0.30, delivery: 90 },
355 '74287': { cost: 265.2, commission: 0.36, delivery: 200 },
356 '75215': { cost: 290.4, commission: 0.36, delivery: 200 },
357 '75079': { cost: 104.4, commission: 0.30, delivery: 90 },
358 '73990': { cost: 252, commission: 0.30, delivery: 90 },
359 '73983': { cost: 273.6, commission: 0.30, delivery: 90 },
360 '76359': { cost: 340.8, commission: 0.36, delivery: 200 },
361 '82090': { cost: 344.4, commission: 0.30, delivery: 90 },
362 '76090': { cost: 192, commission: 0.36, delivery: 200 },
363 '76328': { cost: 338.4, commission: 0.36, delivery: 200 },
364 '74003': { cost: 258, commission: 0.30, delivery: 90 },
365 '76311': { cost: 131.64, commission: 0.30, delivery: 90 },
366 '72597': { cost: 258, commission: 0.30, delivery: 90 },
367 '74089': { cost: 172.8, commission: 0.30, delivery: 90 },
368 '75321': { cost: 175.2, commission: 0.30, delivery: 90 },
369 '73259': { cost: 357.6, commission: 0.30, delivery: 90 },
370 '73211': { cost: 230.052, commission: 0.30, delivery: 90 },
371 '74065': { cost: 205.2, commission: 0.36, delivery: 200 },
372 '74362': { cost: 146.4, commission: 0.30, delivery: 90 },
373 '75598': { cost: 230.4, commission: 0.30, delivery: 90 },
374 '76236': { cost: 285.6, commission: 0.36, delivery: 200 },
375 '76342': { cost: 321.6, commission: 0.36, delivery: 200 },
376 '75086': { cost: 177.6, commission: 0.30, delivery: 90 },
377 '71576': { cost: 128.4, commission: 0.30, delivery: 90 },
378 '74836': { cost: 207.6, commission: 0.30, delivery: 90 },
379 '75314': { cost: 124.8, commission: 0.36, delivery: 200 },
380 '76052': { cost: 144, commission: 0.30, delivery: 90 },
381 '76304': { cost: 324, commission: 0.36, delivery: 200 },
382 '71682': { cost: 99.6, commission: 0.30, delivery: 90 },
383 '74959': { cost: 183.6, commission: 0.30, delivery: 90 },
384 '303978': { cost: 146.4, commission: 0.42, delivery: 105 },
385'302759': { cost: 159.6, commission: 0.42, delivery: 105 },
386'302711': { cost: 111.6, commission: 0.42, delivery: 105 },
387'304067': { cost: 242.4, commission: 0.42, delivery: 105 },
388'303244': { cost: 130.8, commission: 0.42, delivery: 105 },
389'303145': { cost: 115.2, commission: 0.42, delivery: 105 },
390'303930': { cost: 258, commission: 0.42, delivery: 105 },
391'303053': { cost: 117.6, commission: 0.42, delivery: 105 },
392'302940': { cost: 219.6, commission: 0.42, delivery: 105 },
393'303626': { cost: 198, commission: 0.42, delivery: 105 },
394'303046': { cost: 133.2, commission: 0.42, delivery: 105 },
395'303961': { cost: 213.6, commission: 0.42, delivery: 105 },
396'303107': { cost: 115.2, commission: 0.42, delivery: 105 },
397'303411': { cost: 146.4, commission: 0.42, delivery: 105 },
398'303909': { cost: 154.8, commission: 0.42, delivery: 105 },
399'303831': { cost: 216, commission: 0.42, delivery: 105 },
400'302766': { cost: 92.4, commission: 0.42, delivery: 105 },
401'303039': { cost: 118.8, commission: 0.42, delivery: 105 },
402'303770': { cost: 200.4, commission: 0.42, delivery: 105 },
403'303985': { cost: 199.2, commission: 0.42, delivery: 105 },
404'303015': { cost: 188.4, commission: 0.42, delivery: 105 },
405'302926': { cost: 199.2, commission: 0.42, delivery: 105 },
406'302599': { cost: 124.8, commission: 0.42, delivery: 105 },
407'303312': { cost: 147.6, commission: 0.42, delivery: 105 },
408'303213': { cost: 117.6, commission: 0.42, delivery: 105 },
409'302841': { cost: 146.4, commission: 0.42, delivery: 105 },
410'303794': { cost: 236.4, commission: 0.42, delivery: 105 },
411'303121': { cost: 110.4, commission: 0.42, delivery: 105 },
412'303398': { cost: 172.8, commission: 0.42, delivery: 105 },
413'302971': { cost: 182.4, commission: 0.42, delivery: 105 },
414'302902': { cost: 162, commission: 0.42, delivery: 105 },
415'303473': { cost: 151.2, commission: 0.42, delivery: 105 },
416'303466': { cost: 213.6, commission: 0.42, delivery: 105 },
417'303268': { cost: 175.2, commission: 0.42, delivery: 105 },
418'303664': { cost: 138, commission: 0.42, delivery: 105 },
419'303947': { cost: 172.8, commission: 0.42, delivery: 105 },
420'303886': { cost: 157.2, commission: 0.42, delivery: 105 },
421'303169': { cost: 153.6, commission: 0.42, delivery: 105 },
422'303732': { cost: 158.4, commission: 0.42, delivery: 105 },
423'304265': { cost: 291.6, commission: 0.42, delivery: 105 },
424'303275': { cost: 91.2, commission: 0.42, delivery: 105 },
425'302445': { cost: 116.4, commission: 0.42, delivery: 105 },
426'303282': { cost: 158.4, commission: 0.42, delivery: 105 },
427'304272': { cost: 307.2, commission: 0.42, delivery: 105 },
428'303510': { cost: 122.4, commission: 0.42, delivery: 105 },
429'303077': { cost: 158.4, commission: 0.42, delivery: 105 },
430'302872': { cost: 96, commission: 0.42, delivery: 105 },
431'303596': { cost: 680.4, commission: 0.42, delivery: 105 },
432'302889': { cost: 148.8, commission: 0.42, delivery: 105 },
433'302728': { cost: 122.4, commission: 0.42, delivery: 105 },
434'303862': { cost: 217.2, commission: 0.42, delivery: 105 },
435'303008': { cost: 136.8, commission: 0.42, delivery: 105 },
436'303725': { cost: 144, commission: 0.42, delivery: 105 },
437'301851': { cost: 154.8, commission: 0.42, delivery: 105 },
438'303879': { cost: 214.8, commission: 0.42, delivery: 105 },
439'301912': { cost: 130.8, commission: 0.42, delivery: 105 },
440'303114': { cost: 120, commission: 0.42, delivery: 105 },
441'303091': { cost: 140.4, commission: 0.42, delivery: 105 },
442'303688': { cost: 105.6, commission: 0.42, delivery: 105 },
443'303084': { cost: 152.4, commission: 0.42, delivery: 105 },
444'302773': { cost: 140.4, commission: 0.42, delivery: 105 },
445'303381': { cost: 117.6, commission: 0.42, delivery: 105 },
446'302797': { cost: 141.6, commission: 0.42, delivery: 105 },
447'302568': { cost: 122.4, commission: 0.42, delivery: 105 },
448'303824': { cost: 147.6, commission: 0.42, delivery: 105 },
449'303633': { cost: 148.8, commission: 0.42, delivery: 105 },
450'303503': { cost: 472.8, commission: 0.42, delivery: 105 },
451'303923': { cost: 166.8, commission: 0.42, delivery: 105 },
452'303152': { cost: 141.6, commission: 0.42, delivery: 105 },
453'302438': { cost: 133.2, commission: 0.42, delivery: 105 },
454'302896': { cost: 156, commission: 0.42, delivery: 105 },
455'302452': { cost: 109.2, commission: 0.42, delivery: 105 },
456'303756': { cost: 111.6, commission: 0.42, delivery: 105 },
457'303718': { cost: 211.2, commission: 0.42, delivery: 105 },
458'302513': { cost: 217.2, commission: 0.42, delivery: 105 },
459'303251': { cost: 135.6, commission: 0.42, delivery: 105 },
460'302261': { cost: 135.6, commission: 0.42, delivery: 105 },
461'302995': { cost: 110.4, commission: 0.42, delivery: 105 },
462'303916': { cost: 164.4, commission: 0.42, delivery: 105 },
463'302865': { cost: 136.8, commission: 0.42, delivery: 105 },
464'302803': { cost: 111.6, commission: 0.42, delivery: 105 },
465'301929': { cost: 105.6, commission: 0.42, delivery: 105 },
466'303534': { cost: 129.6, commission: 0.42, delivery: 105 },
467'302810': { cost: 132, commission: 0.42, delivery: 105 },
468'303138': { cost: 123.6, commission: 0.42, delivery: 105 },
469'303367': { cost: 153.6, commission: 0.42, delivery: 105 },
470'304081': { cost: 156, commission: 0.42, delivery: 105 },
471'302988': { cost: 180, commission: 0.42, delivery: 105 },
472'301905': { cost: 121.2, commission: 0.42, delivery: 105 },
473'303220': { cost: 147.6, commission: 0.42, delivery: 105 },
474'304296': { cost: 260.4, commission: 0.42, delivery: 105 },
475'303763': { cost: 105.6, commission: 0.42, delivery: 105 },
476'303565': { cost: 145.2, commission: 0.42, delivery: 105 },
477'304227': { cost: 120, commission: 0.42, delivery: 105 },
478'302698': { cost: 120, commission: 0.42, delivery: 105 },
479'303176': { cost: 171.6, commission: 0.42, delivery: 105 },
480'302650': { cost: 108, commission: 0.42, delivery: 105 },
481'303671': { cost: 172.8, commission: 0.42, delivery: 105 },
482'302933': { cost: 108, commission: 0.42, delivery: 105 },
483'303183': { cost: 201.6, commission: 0.42, delivery: 105 },
484'302674': { cost: 116.4, commission: 0.42, delivery: 105 },
485'302742': { cost: 166.8, commission: 0.42, delivery: 105 },
486'303442': { cost: 144, commission: 0.42, delivery: 105 },
487'303060': { cost: 93.6, commission: 0.42, delivery: 105 },
488'303701': { cost: 144, commission: 0.42, delivery: 105 },
489'303374': { cost: 218.4, commission: 0.42, delivery: 105 },
490'303299': { cost: 132, commission: 0.42, delivery: 105 },
491'302421': { cost: 162, commission: 0.42, delivery: 105 },
492'302551': { cost: 112.8, commission: 0.42, delivery: 105 },
493'303657': { cost: 114, commission: 0.42, delivery: 105 },
494'300359': { cost: 110.4, commission: 0.42, delivery: 105 },
495'302629': { cost: 105.6, commission: 0.42, delivery: 105 },
496'302582': { cost: 111.6, commission: 0.42, delivery: 105 },
497'304364': { cost: 123.6, commission: 0.42, delivery: 105 },
498'300373': { cost: 109.2, commission: 0.42, delivery: 105 },
499'303817': { cost: 189.6, commission: 0.42, delivery: 105 },
500'303350': { cost: 127.2, commission: 0.42, delivery: 105 },
501'304074': { cost: 154.8, commission: 0.42, delivery: 105 },
502'303206': { cost: 117.6, commission: 0.42, delivery: 105 },
503'302506': { cost: 111.6, commission: 0.42, delivery: 105 },
504'303695': { cost: 111.6, commission: 0.42, delivery: 105 },
505'303749': { cost: 159.6, commission: 0.42, delivery: 105 },
506'302407': { cost: 146.4, commission: 0.42, delivery: 105 },
507'303480': { cost: 145.2, commission: 0.42, delivery: 105 },
508'303459': { cost: 144, commission: 0.42, delivery: 105 },
509'302537': { cost: 94.8, commission: 0.42, delivery: 105 },
510'303527': { cost: 126, commission: 0.42, delivery: 105 },
511'303848': { cost: 142.8, commission: 0.42, delivery: 105 },
512'303800': { cost: 175.2, commission: 0.42, delivery: 105 },
513'300366': { cost: 112.8, commission: 0.42, delivery: 105 },
514'302780': { cost: 138, commission: 0.42, delivery: 105 },
515'302544': { cost: 133.2, commission: 0.42, delivery: 105 },
516'302827': { cost: 129.6, commission: 0.42, delivery: 105 },
517'302483': { cost: 135.6, commission: 0.42, delivery: 105 },
518'302858': { cost: 157.2, commission: 0.42, delivery: 105 },
519'303190': { cost: 127.2, commission: 0.42, delivery: 105 },
520'302681': { cost: 123.6, commission: 0.42, delivery: 105 },
521'302575': { cost: 109.2, commission: 0.42, delivery: 105 },
522'302735': { cost: 120, commission: 0.42, delivery: 105 },
523'302919': { cost: 145.2, commission: 0.42, delivery: 105 },
524'302469': { cost: 153.6, commission: 0.42, delivery: 105 },
525'302490': { cost: 126, commission: 0.42, delivery: 105 },
526'303435': { cost: 94.8, commission: 0.42, delivery: 105 },
527'303640': { cost: 118.8, commission: 0.42, delivery: 105 },
528'304104': { cost: 94.8, commission: 0.42, delivery: 105 },
529'303428': { cost: 92.4, commission: 0.42, delivery: 105 },
530'304036': { cost: 669.6, commission: 0.42, delivery: 105 },
531'304302': { cost: 294, commission: 0.42, delivery: 105 },
532'303954': { cost: 213.6, commission: 0.42, delivery: 105 },
533'304289': { cost: 348, commission: 0.42, delivery: 105 },
534'304043': { cost: 535.2, commission: 0.42, delivery: 105 },
535'303589': { cost: 598.8, commission: 0.42, delivery: 105 },
536'303572': { cost: 624, commission: 0.42, delivery: 105 },
537'303497': { cost: 805.2, commission: 0.42, delivery: 105 },
538'303992': { cost: 711.6, commission: 0.42, delivery: 105 },
539'304050': { cost: 613.2, commission: 0.42, delivery: 105 },
540'304005': { cost: 598.8, commission: 0.42, delivery: 105 },
541'303541': { cost: 568.8, commission: 0.42, delivery: 105 },
542'304012': { cost: 567.6, commission: 0.42, delivery: 105 },
543'303558': { cost: 487.2, commission: 0.42, delivery: 105 },
544'304029': { cost: 476.4, commission: 0.42, delivery: 105 },
545'303237': { cost: 118.8, commission: 0.42, delivery: 105 },
546'303893': { cost: 309.6, commission: 0.42, delivery: 105 },
547'303305': { cost: 175.2, commission: 0.42, delivery: 105 }
548
549 };
550
551
552 // Функция расчета прибыли
553
554 function calculateProfit(article, revenue, orders, drr) {
555
556 const costData = PRODUCT_COST_DATA[article];
557
558 if (!costData || !revenue || !orders) return null;
559
560
561
562 // Расходы на рекламу = выручка * (ДРР / 100)
563
564 const adCost = drr ? (revenue * (drr / 100)) : 0;
565
566
567
568 // Прибыль = Выручка - (заказы * себестоимость) - (заказы * доставка) - (выручка * комиссия) - расходы на рекламу
569
570 const profit = revenue - (orders * costData.cost) - (orders * costData.delivery) - (revenue * costData.commission) - adCost;
571
572 return Math.round(profit); // Округляем до целых
573
574 }
575
576
577 // Класс для сбора данных о товарах
578
579 class ProductDataCollector {
580
581 constructor() {
582
583 this.products = [];
584
585 this.isCollecting = false;
586
587 this.analysisPeriodDays = 7; // По умолчанию 7 дней
588
589 }
590
591
592 // Определение периода анализа из интерфейса
593 detectAnalysisPeriod() {
594 try {
595 console.log('🔍 Ищем период анализа в интерфейсе...');
596
597 // ПРИОРИТЕТ 1: Проверяем активную кнопку периода с полными датами
598 const periodButtons = document.querySelectorAll('button[data-active="true"]');
599 console.log(`🔘 Найдено активных кнопок: ${periodButtons.length}`);
600
601 for (const button of periodButtons) {
602 const buttonText = button.textContent.trim();
603 console.log(`🔘 Проверяем кнопку: "${buttonText}"`);
604
605 // Проверяем кнопки с полным диапазоном дат (например "24 – 30 ноября 2025")
606 // Используем \s для любых пробелов (включая ) и [-–—] для любых тире
607 const fullDateMatch = buttonText.match(/(\d{1,2})\s*[-–—]\s*(\d{1,2})\s+(января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\s+(\d{4})/i);
608 if (fullDateMatch) {
609 const startDay = parseInt(fullDateMatch[1]);
610 const endDay = parseInt(fullDateMatch[2]);
611 const days = endDay - startDay + 1;
612
613 console.log(`📅 Найден диапазон дат: ${startDay} - ${endDay} = ${days} дней`);
614
615 if (days > 0 && days <= 365) {
616 this.analysisPeriodDays = days;
617 console.log(`✅ Определен период по кнопке с полной датой: ${days} дней (${startDay}-${endDay})`);
618 return this.analysisPeriodDays;
619 }
620 }
621
622 // Маппинг кнопок на количество дней
623 const periodMap = {
624 'Сегодня': 1,
625 'Вчера': 1,
626 '7 дней': 7,
627 '28 дней': 28,
628 'Квартал': 90,
629 'Год': 365
630 };
631
632 if (periodMap[buttonText]) {
633 this.analysisPeriodDays = periodMap[buttonText];
634 console.log(`✅ Определен период по кнопке: ${this.analysisPeriodDays} дней (кнопка: "${buttonText}")`);
635 return this.analysisPeriodDays;
636 }
637 }
638
639 // ПРИОРИТЕТ 2: Ищем диапазон дат в тексте страницы
640 const allText = document.body.innerText;
641 console.log('🔍 Ищем даты в тексте страницы...');
642
643 // Паттерн: "30 нояб — 6 дек" или "9 нояб — 6 дек" или "07 сент — 6 дек"
644 let dateRangeMatch = allText.match(/(\d{1,2})\s*(янв|фев|мар|апр|мая|май|июн|июл|авг|сен|сент|окт|ноя|дек)[а-я]*\s*[-—–]\s*(\d{1,2})\s*(янв|фев|мар|апр|мая|май|июн|июл|авг|сен|сент|окт|ноя|дек)[а-я]*/i);
645
646 if (dateRangeMatch) {
647 const startDay = parseInt(dateRangeMatch[1]);
648 const endDay = parseInt(dateRangeMatch[3]);
649 const startMonth = dateRangeMatch[2].toLowerCase();
650 const endMonth = dateRangeMatch[4].toLowerCase();
651
652 console.log(`📅 Найдены даты: ${startDay} ${startMonth} — ${endDay} ${endMonth}`);
653
654 // Маппинг месяцев
655 const monthMap = {
656 'янв': 1, 'фев': 2, 'мар': 3, 'апр': 4, 'мая': 5, 'май': 5,
657 'июн': 6, 'июл': 7, 'авг': 8, 'сен': 9, 'сент': 9, 'окт': 10, 'ноя': 11, 'дек': 12
658 };
659
660 const startMonthNum = monthMap[startMonth.substring(0, 3)];
661 const endMonthNum = monthMap[endMonth.substring(0, 3)];
662
663 console.log(`📅 Месяцы: ${startMonthNum} → ${endMonthNum}`);
664
665 let days;
666
667 if (startMonthNum === endMonthNum) {
668 // Даты в одном месяце
669 days = endDay - startDay + 1;
670 console.log(`📅 Один месяц: ${days} дней`);
671 } else {
672 // Разные месяцы - считаем точно
673 const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
674
675 // Дни от начальной даты до конца начального месяца
676 const daysInStartMonth = daysInMonth[startMonthNum - 1];
677 const daysFromStart = daysInStartMonth - startDay + 1;
678
679 // Дни полных месяцев между начальным и конечным
680 let daysInBetween = 0;
681 let currentMonth = startMonthNum;
682 while (true) {
683 currentMonth++;
684 if (currentMonth > 12) currentMonth = 1; // Переход через год
685 if (currentMonth === endMonthNum) break;
686 daysInBetween += daysInMonth[currentMonth - 1];
687 }
688
689 // Дни конечного месяца
690 const daysInEndMonth = endDay;
691
692 days = daysFromStart + daysInBetween + daysInEndMonth;
693 console.log(`📅 Несколько месяцев: ${daysFromStart} + ${daysInBetween} + ${daysInEndMonth} = ${days} дней`);
694 }
695
696 if (days > 0 && days <= 365) {
697 this.analysisPeriodDays = days;
698 console.log(`✅ Определен период анализа: ${days} дней`);
699 return days;
700 }
701 }
702
703 // Паттерн 3: Ищем текст типа "за 7 дней", "за 14 дней", "за 28 дней"
704 const periodMatch = allText.match(/за\s+(\d+)\s+дн/i);
705 if (periodMatch) {
706 const days = parseInt(periodMatch[1]);
707 if (days > 0 && days <= 365) {
708 this.analysisPeriodDays = days;
709 console.log(`✅ Определен период анализа: ${days} дней`);
710 return days;
711 }
712 }
713
714 console.log(`⚠️ Период анализа не определен, используем по умолчанию: ${this.analysisPeriodDays} дней`);
715 return this.analysisPeriodDays;
716 } catch (error) {
717 console.error('Ошибка определения периода:', error);
718 return this.analysisPeriodDays;
719 }
720 }
721
722
723 // Автоматическая подгрузка всех товаров
724
725 async loadAllProducts() {
726
727 console.log('📦 Начинаем загрузку всех товаров...');
728
729 this.isCollecting = true;
730
731
732 let previousCount = 0;
733
734 let stableCount = 0; // Счетчик стабильных попыток
735
736 let attempts = 0;
737
738 const maxAttempts = 300; // Увеличили максимум попыток до 300
739
740 const maxStableAttempts = 5; // Увеличили до 5 стабильных попыток
741
742
743 while (attempts < maxAttempts) {
744
745 const loadMoreBtn = document.querySelector('button.styles_loadMoreButton_2RI3D');
746
747
748
749 if (!loadMoreBtn) {
750
751 console.log('✅ Кнопка "Показать ещё" не найдена - все товары загружены');
752
753 break;
754
755 }
756
757
758 // Проверяем, не отключена ли кнопка
759
760 if (loadMoreBtn.disabled || loadMoreBtn.classList.contains('disabled')) {
761
762 console.log('✅ Кнопка "Показать ещё" отключена - все товары загружены');
763
764 break;
765
766 }
767
768
769 // Проверяем, есть ли товары с нулевой выручкой (значит дошли до конца)
770
771 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
772
773 let hasZeroRevenue = false;
774
775
776
777 for (const row of rows) {
778
779 const cells = row.querySelectorAll('td');
780
781 if (cells.length >= 3) {
782
783 const revenueText = cells[2].textContent.trim();
784
785 // Проверяем, есть ли "0 ₽" или "0₽" в тексте выручки
786
787 if (revenueText.match(/^0\s*₽/) || revenueText === '0') {
788
789 hasZeroRevenue = true;
790
791 console.log('✅ Найден товар с нулевой выручкой - останавливаем загрузку');
792
793 break;
794
795 }
796
797 }
798
799 }
800
801
802
803 if (hasZeroRevenue) {
804
805 console.log('✅ Достигнут конец списка активных товаров');
806
807 break;
808
809 }
810
811
812 // Прокручиваем к кнопке, чтобы она была видна
813
814 loadMoreBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
815
816 await delay(800);
817
818
819 console.log(`🔄 Клик по кнопке "Показать ещё" (попытка ${attempts + 1})`);
820
821 loadMoreBtn.click();
822
823
824
825 // Увеличили задержку до 4 секунд для полной загрузки данных
826
827 await delay(4000);
828
829
830 const currentCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
831
832 console.log(`📊 Загружено товаров: ${currentCount} (было: ${previousCount})`);
833
834
835 if (currentCount === previousCount) {
836
837 stableCount++;
838
839 console.log(`⏸️ Количество не изменилось (${stableCount}/${maxStableAttempts})`);
840
841
842
843 if (stableCount >= maxStableAttempts) {
844
845 console.log('✅ Количество товаров стабильно - загрузка завершена');
846
847 break;
848
849 }
850
851 } else {
852
853 stableCount = 0; // Сбрасываем счетчик, если количество изменилось
854
855 }
856
857
858 previousCount = currentCount;
859
860 attempts++;
861
862 }
863
864
865 const finalCount = document.querySelectorAll('tr.ct590-c0.ct590-b9').length;
866
867 console.log(`✅ Загрузка завершена. Всего товаров: ${finalCount}`);
868
869 this.isCollecting = false;
870
871 }
872
873
874 // Сбор данных из таблицы
875
876 collectProductData() {
877
878 console.log('📊 Собираем данные о товарах...');
879
880 this.products = [];
881
882
883 // Определяем период анализа
884 this.detectAnalysisPeriod();
885
886
887 const rows = document.querySelectorAll('tr.ct590-c0.ct590-b9');
888
889 console.log(`Найдено строк: ${rows.length}`);
890
891
892 rows.forEach((row, index) => {
893
894 try {
895
896 const cells = row.querySelectorAll('td');
897
898 if (cells.length < 10) return;
899
900
901 // Извлекаем данные из ячеек
902
903 const productData = this.extractProductData(cells);
904
905 if (productData) {
906
907 this.products.push(productData);
908
909 }
910
911 } catch (error) {
912
913 console.error(`Ошибка при обработке строки ${index}:`, error);
914
915 }
916
917 });
918
919
920 console.log(`✅ Собрано товаров: ${this.products.length}`);
921
922 return this.products;
923
924 }
925
926
927 // Извлечение данных о товаре из ячеек
928
929 extractProductData(cells) {
930
931 try {
932
933 // Название и артикул (первая ячейка)
934
935 const nameCell = cells[0];
936
937 const nameLink = nameCell.querySelector('a.styles_productName_2qRJi');
938
939 const captionEl = nameCell.querySelector('.styles_productCaption_7MqtH');
940
941
942
943 const name = nameLink ? nameLink.textContent.trim() : '';
944
945 const articleMatch = captionEl ? captionEl.textContent.match(/Арт\.\s*(\d+)/) : null;
946
947 const article = articleMatch ? articleMatch[1] : '';
948
949
950 if (!name || !article) return null;
951
952
953 // Получаем текстовое содержимое всех ячеек
954
955 const cellTexts = Array.from(cells).map(cell => cell.textContent.trim());
956
957
958 // Парсим основные показатели по правильным индексам
959
960 // Выручка - индекс 2
961
962 const revenue = parseNumber(cellTexts[2]);
963
964 const revenueChange = parsePercent(cellTexts[2]);
965
966
967
968 // Заказано товаров - индекс 20
969 // ИСПРАВЛЕНИЕ: Парсим из внутреннего div, а не из textContent всей ячейки
970 const ordersContentDiv = cells[20].querySelector('.styles_content_2N0WE > div:not(.styles_tooltip_2XG5L)');
971 const ordersText = ordersContentDiv ? ordersContentDiv.textContent.trim() : cellTexts[20];
972 const orders = parseNumber(ordersText);
973
974 const ordersChange = parsePercent(cellTexts[20]);
975
976
977
978 // Показы всего - индекс 5
979
980 const impressions = parseNumber(cellTexts[5]);
981
982 const impressionsChange = parsePercent(cellTexts[5]);
983
984
985
986 // Посещения карточки товара - индекс 13
987
988 const cardVisits = parseNumber(cellTexts[13]);
989
990 const cardVisitsChange = parsePercent(cellTexts[13]);
991
992
993
994 // Конверсия из поиска и каталога в карточку (CTR) - индекс 12
995
996 const conversionCatalogToCard = parseNumber(cellTexts[12]);
997
998 const conversionCatalogToCardChange = parsePercent(cellTexts[12]);
999
1000
1001
1002 // Конверсия из карточки в корзину (CRL) - индекс 15
1003
1004 const conversionCardToCart = parseNumber(cellTexts[15]);
1005
1006 const conversionCardToCartChange = parsePercent(cellTexts[15]);
1007
1008
1009
1010 // Добавления в корзину всего - индекс 18
1011
1012 const cartAdditions = parseNumber(cellTexts[18]);
1013
1014 const cartAdditionsChange = parsePercent(cellTexts[18]);
1015
1016
1017
1018 // CR - высчитываем: Заказано товаров / Посещения карточки товаров
1019
1020 const cr = (orders && cardVisits && cardVisits > 0) ? parseFloat(((orders / cardVisits) * 100).toFixed(1)) : null;
1021
1022 const crChange = null; // Изменение CR нужно высчитывать отдельно
1023
1024
1025
1026 // Общая ДРР - индекс 32 (парсим как процент, убираем знак %)
1027
1028 const drrText = cellTexts[32] || '';
1029
1030 const drrMatch = drrText.match(/(\d+(?:\.\d+)?)\s*%/);
1031
1032 const drr = drrMatch ? parseFloat(drrMatch[1]) : null;
1033
1034 const drrChange = parsePercent(cellTexts[32]);
1035
1036
1037
1038 // Остаток на конец периода - индекс 35
1039
1040 const stockText = cellTexts[35] || '';
1041
1042 const stockMatch = stockText.match(/(\d+)/);
1043
1044 const stock = stockMatch ? parseInt(stockMatch[1]) : null;
1045
1046
1047
1048 // Средняя цена - индекс 28 (используем parsePrice для корректного парсинга)
1049
1050 const avgPrice = parsePrice(cellTexts[28]);
1051
1052 const avgPriceChange = parsePercent(cellTexts[28]);
1053
1054
1055
1056 // Среднее время доставки - индекс 37
1057
1058 const deliveryTime = cellTexts[37] || null;
1059
1060
1061
1062 // Рассчитываем прибыль
1063
1064 const profit = calculateProfit(article, revenue, orders, drr);
1065
1066
1067
1068 // Рассчитываем прибыль в процентах от выручки
1069 const profitPercent = (profit !== null && revenue && revenue > 0) ?
1070 parseFloat(((profit / revenue) * 100).toFixed(1)) : null;
1071
1072 // Рассчитываем комиссию и себестоимость
1073 const costData = PRODUCT_COST_DATA[article];
1074 const totalCommission = costData && revenue ?
1075 Math.round((orders * costData.delivery) + (revenue * costData.commission)) : null;
1076 const totalCommissionPercent = (totalCommission !== null && revenue && revenue > 0) ?
1077 parseFloat(((totalCommission / revenue) * 100).toFixed(1)) : null;
1078
1079 const totalCost = costData && orders ? Math.round(orders * costData.cost) : null;
1080 const totalCostPercent = (totalCost !== null && revenue && revenue > 0) ?
1081 parseFloat(((totalCost / revenue) * 100).toFixed(1)) : null;
1082
1083 // ИСПРАВЛЕНИЕ: Рассчитываем "на дней" правильно
1084 // Для периода в 1 день (Вчера/Сегодня): остаток / заказы = дней
1085 // Для периода >1 дня: остаток / (заказы / период) = дней
1086 let daysOfStock = null;
1087 if (orders && stock !== null && orders > 0 && this.analysisPeriodDays > 0) {
1088 const avgDailyOrders = orders / this.analysisPeriodDays;
1089 daysOfStock = Math.floor(stock / avgDailyOrders);
1090 }
1091
1092 // Логируем расчет для отладки
1093 console.log(`📊 Артикул ${article}: остаток=${stock}, заказы=${orders}, период=${this.analysisPeriodDays} дней, среднедневные заказы=${orders && this.analysisPeriodDays > 0 ? (orders / this.analysisPeriodDays).toFixed(2) : 'н/д'}, на дней=${daysOfStock}`);
1094
1095
1096 const product = {
1097
1098 name,
1099
1100 article,
1101
1102 revenue,
1103
1104 revenueChange,
1105
1106 orders,
1107
1108 ordersChange,
1109
1110 impressions,
1111
1112 impressionsChange,
1113
1114 cardVisits,
1115
1116 cardVisitsChange,
1117
1118 conversionCatalogToCard,
1119
1120 conversionCatalogToCardChange,
1121
1122 conversionCardToCart,
1123
1124 conversionCardToCartChange,
1125
1126 cartAdditions,
1127
1128 cartAdditionsChange,
1129
1130 cr,
1131
1132 crChange,
1133
1134 avgPrice,
1135
1136 avgPriceChange,
1137
1138 drr,
1139
1140 drrChange,
1141
1142 stock,
1143
1144 deliveryTime,
1145
1146 daysOfStock,
1147
1148 profit,
1149
1150 profitPercent,
1151
1152 totalCommission,
1153
1154 totalCommissionPercent,
1155
1156 totalCost,
1157
1158 totalCostPercent,
1159
1160 rawData: cellTexts
1161
1162 };
1163
1164
1165 return product;
1166
1167 } catch (error) {
1168
1169 console.error('Ошибка извлечения данных товара:', error);
1170
1171 return null;
1172
1173 }
1174
1175 }
1176
1177 }
1178
1179
1180 // Класс для AI анализа
1181
1182 class AIAnalyzer {
1183
1184 // Батч-анализ товаров с умной фильтрацией
1185
1186 async analyzeProducts(products, onProgress) {
1187
1188 console.log('🤖 Начинаем AI анализ товаров...');
1189
1190
1191
1192 // Сначала вычисляем средние показатели
1193
1194 const avgMetrics = this.calculateAverageMetrics(products);
1195
1196 console.log('📊 Средние показатели:', avgMetrics);
1197
1198
1199
1200 // Разделяем товары на приоритетные и обычные
1201
1202 const priorityProducts = [];
1203
1204 const normalProducts = [];
1205
1206
1207
1208 products.forEach(product => {
1209
1210 const needsAIAnalysis = this.needsDetailedAnalysis(product, avgMetrics);
1211
1212 if (needsAIAnalysis) {
1213
1214 priorityProducts.push(product);
1215
1216 } else {
1217
1218 normalProducts.push(product);
1219
1220 }
1221
1222 });
1223
1224
1225
1226 console.log(`📊 Приоритетных товаров для AI анализа: ${priorityProducts.length}`);
1227
1228 console.log(`📊 Обычных товаров (базовый анализ): ${normalProducts.length}`);
1229
1230
1231
1232 const analyzedProducts = [];
1233
1234 const batchSize = 10; // Увеличили до 10 товаров одновременно
1235
1236
1237
1238 // Сначала быстро обрабатываем обычные товары (без AI)
1239
1240 normalProducts.forEach(product => {
1241
1242 analyzedProducts.push({
1243
1244 ...product,
1245
1246 analysis: this.basicAnalysis(product, avgMetrics)
1247
1248 });
1249
1250 });
1251
1252
1253
1254 // Обновляем прогресс после базового анализа
1255
1256 if (onProgress) {
1257
1258 const percentage = Math.round((normalProducts.length / products.length) * 100);
1259
1260 const remaining = Math.ceil((priorityProducts.length / batchSize) * 2);
1261
1262 onProgress(normalProducts.length, products.length, percentage, remaining);
1263
1264 }
1265
1266
1267
1268 // Анализируем приоритетные товары с AI
1269
1270 for (let i = 0; i < priorityProducts.length; i += batchSize) {
1271
1272 const batch = priorityProducts.slice(i, i + batchSize);
1273
1274 const batchPromises = batch.map(product => this.analyzeProduct(product, avgMetrics, true));
1275
1276
1277
1278 const batchResults = await Promise.all(batchPromises);
1279
1280
1281
1282 batchResults.forEach((analysis, idx) => {
1283
1284 analyzedProducts.push({
1285
1286 ...batch[idx],
1287
1288 analysis
1289
1290 });
1291
1292 });
1293
1294
1295
1296 const progress = Math.min(i + batchSize, priorityProducts.length);
1297
1298 const totalProgress = normalProducts.length + progress;
1299
1300 const percentage = Math.round((totalProgress / products.length) * 100);
1301
1302 const remaining = Math.ceil(((priorityProducts.length - progress) / batchSize) * 2);
1303
1304
1305
1306 if (onProgress) {
1307
1308 onProgress(totalProgress, products.length, percentage, remaining);
1309
1310 }
1311
1312
1313
1314 console.log(`✅ Проанализировано ${progress} из ${priorityProducts.length} приоритетных товаров`);
1315
1316 }
1317
1318
1319
1320 if (onProgress) {
1321
1322 onProgress(products.length, products.length, 100, 0);
1323
1324 }
1325
1326
1327 return analyzedProducts;
1328
1329 }
1330
1331
1332 // Базовый анализ всех товаров без AI
1333 async analyzeProductsBasic(products, onProgress) {
1334 console.log('📊 Начинаем базовый анализ товаров (без AI)...');
1335
1336 const avgMetrics = this.calculateAverageMetrics(products);
1337 console.log('📊 Средние показатели:', avgMetrics);
1338
1339 const analyzedProducts = [];
1340
1341 products.forEach((product, index) => {
1342 analyzedProducts.push({
1343 ...product,
1344 analysis: this.basicAnalysis(product, avgMetrics)
1345 });
1346
1347 // Обновляем прогресс
1348 if (onProgress && (index % 10 === 0 || index === products.length - 1)) {
1349 const percentage = Math.round(((index + 1) / products.length) * 100);
1350 onProgress(index + 1, products.length, percentage, 0);
1351 }
1352 });
1353
1354 console.log(`✅ Базовый анализ завершен: ${analyzedProducts.length} товаров`);
1355 return analyzedProducts;
1356 }
1357
1358
1359 // Определяем, нужен ли детальный AI анализ
1360
1361 needsDetailedAnalysis(product, avgMetrics) {
1362
1363 const threshold = 5; // Порог отклонения 5%
1364
1365
1366
1367 // Если есть значительное падение выручки
1368
1369 if (product.revenueChange !== null && product.revenueChange < avgMetrics.revenueChange - threshold) {
1370
1371 return true;
1372
1373 }
1374
1375
1376
1377 // Если есть значительное падение заказов
1378
1379 if (product.ordersChange !== null && product.ordersChange < avgMetrics.ordersChange - threshold) {
1380
1381 return true;
1382
1383 }
1384
1385
1386
1387 // Если высокий ДРР
1388
1389 if (product.drr !== null && product.drr > 20) {
1390
1391 return true;
1392
1393 }
1394
1395
1396
1397 // Если низкие остатки
1398
1399 const daysOfStock = product.daysOfStock;
1400
1401 if (daysOfStock !== null && daysOfStock < 49) {
1402
1403 return true;
1404
1405 }
1406
1407
1408
1409 // Если значительный рост (для масштабирования)
1410
1411 if (product.revenueChange !== null && product.revenueChange > avgMetrics.revenueChange + 15) {
1412
1413 return true;
1414
1415 }
1416
1417
1418
1419 return false;
1420
1421 }
1422
1423
1424 // Базовый анализ без AI (для товаров без проблем)
1425
1426 basicAnalysis(product, avgMetrics) {
1427
1428 const daysOfStock = product.daysOfStock;
1429
1430 const isLowStock = daysOfStock !== null && daysOfStock <= 14;
1431
1432 const isHighDRR = product.drr !== null && product.drr > 20;
1433
1434 const isOutOfStock = product.stock === 0 || product.stock === null || (daysOfStock !== null && daysOfStock < 2);
1435
1436
1437 const isLowDRR = product.drr !== null && product.drr <= 17;
1438
1439 const isGrowth = this.detectGrowth(product, avgMetrics);
1440
1441 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
1442
1443 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
1444
1445 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
1446
1447 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
1448
1449 (product.profit / product.revenue) < 0.25;
1450
1451
1452
1453 // Проверяем время доставки (парсим число из строки типа "35 ч")
1454
1455 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
1456
1457 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
1458
1459
1460 // Генерируем рекомендации на основе проблем
1461
1462 const recommendations = [];
1463
1464
1465 if (isOutOfStock) {
1466
1467 recommendations.push('Out-of-stock - Срочно поставить товар!');
1468
1469 }
1470
1471
1472
1473 if (isLowStock) {
1474
1475 recommendations.push('Низкие остатки - Поставить товар, повысить цену, снизить ДРР');
1476
1477 }
1478
1479
1480 if (isHighDRR) {
1481
1482 recommendations.push('Высокий ДРР - Понизить ДРР, снизить цену');
1483
1484 }
1485
1486
1487 if (isLowDRR) {
1488
1489 recommendations.push('Повысить ДРР - Повысить ДРР, повысить цену');
1490
1491 }
1492
1493
1494 if (isLowImpressions) {
1495
1496 recommendations.push('Упали Показы - Проверить остатки, повысить ДРР, снизить цену');
1497
1498 }
1499
1500
1501
1502 if (isLowCR) {
1503
1504 recommendations.push('Повысить CR - Проверить остатки, снизить цену');
1505
1506 }
1507
1508
1509
1510 if (isLowProfit) {
1511
1512 recommendations.push('Низкая прибыль - снизить ДРР, проверить цену');
1513
1514 }
1515
1516
1517
1518 if (isBadDeliveryTime) {
1519
1520 recommendations.push('Плохое время - проверить остатки, сделать поставку');
1521
1522 }
1523
1524
1525 if (isGrowth) {
1526
1527 recommendations.push('Рост - поднять цену');
1528
1529 }
1530
1531
1532
1533 // Если нет проблем - выводим "Всё хорошо"
1534
1535 if (recommendations.length === 0) {
1536
1537 recommendations.push('Всё хорошо, рекомендаций нет');
1538
1539 }
1540
1541
1542
1543 return {
1544
1545 priority: 'low',
1546
1547 problems: [],
1548
1549 recommendations,
1550
1551 daysOfStock,
1552
1553 isLowStock,
1554
1555 isHighDRR,
1556
1557 isLowDRR,
1558
1559 isGrowth,
1560
1561 isLowImpressions,
1562
1563 isLowCR,
1564
1565 isLowProfit,
1566
1567 isBadDeliveryTime,
1568
1569 isOutOfStock
1570
1571 };
1572
1573 }
1574
1575
1576 // Вычисление средних показателей
1577
1578 calculateAverageMetrics(products) {
1579
1580 const validProducts = products.filter(p => p.revenueChange !== null);
1581
1582 if (validProducts.length === 0) return { revenueChange: 0, ordersChange: 0, impressionsChange: 0 };
1583
1584
1585
1586 const sum = validProducts.reduce((acc, p) => ({
1587
1588 revenueChange: acc.revenueChange + (p.revenueChange || 0),
1589
1590 ordersChange: acc.ordersChange + (p.ordersChange || 0),
1591
1592 impressionsChange: acc.impressionsChange + (p.impressionsChange || 0)
1593
1594 }), { revenueChange: 0, ordersChange: 0, impressionsChange: 0 });
1595
1596
1597
1598 return {
1599
1600 revenueChange: sum.revenueChange / validProducts.length,
1601
1602 ordersChange: sum.ordersChange / validProducts.length,
1603
1604 impressionsChange: sum.impressionsChange / validProducts.length
1605
1606 };
1607
1608 }
1609
1610
1611 async analyzeProduct(product, avgMetrics, useAI = true) {
1612
1613 try {
1614
1615 // Используем уже рассчитанное значение daysOfStock из product
1616 const daysOfStock = product.daysOfStock;
1617
1618 const isLowStock = daysOfStock !== null && daysOfStock <= 14;
1619
1620 const isHighDRR = product.drr !== null && product.drr > 20;
1621
1622 const isOutOfStock = product.stock === 0 || product.stock === null || (daysOfStock !== null && daysOfStock < 2);
1623
1624
1625 const isLowDRR = product.drr !== null && product.drr <= 17;
1626
1627 const isGrowth = this.detectGrowth(product, avgMetrics);
1628
1629 const isLowImpressions = product.impressionsChange !== null && product.impressionsChange <= -20;
1630
1631 const isLowCR = (product.conversionCardToCartChange !== null && product.conversionCardToCartChange <= -20) ||
1632
1633 (product.conversionCatalogToCardChange !== null && product.conversionCatalogToCardChange <= -20);
1634
1635 const isLowProfit = product.profit !== null && product.revenue !== null && product.revenue > 0 &&
1636
1637 (product.profit / product.revenue) < 0.25;
1638
1639
1640
1641 // Проверяем время доставки
1642
1643 const deliveryHours = product.deliveryTime ? parseInt(product.deliveryTime) : null;
1644
1645 const isBadDeliveryTime = deliveryHours !== null && deliveryHours >= 35;
1646
1647
1648
1649 if (!useAI) {
1650
1651 return this.basicAnalysis(product, avgMetrics);
1652
1653 }
1654
1655
1656
1657 // Формируем промпт для AI
1658
1659 const prompt = `Ты — AI‑аналитик маркетплейса Ozon. На вход ты получаешь показатели одного товара за выбранный период и их динамику к прошлому периоду.
1660
1661Метрики, которые ты можешь видеть:
1662- выручка, прибыль, маржа в %;
1663- заказы, корзины;
1664- показы, клики/переходы в карточку, CTR, CR, CRL;
1665- рекламные расходы, ДРР;
1666- цена;
1667- остаток в штуках, показатель «хватит на дней»;
1668- время доставки (в часах);
1669- динамика каждого показателя к прошлому периоду в % (например, «-7%»).
1670
1671Других данных (рейтинг, отзывы, акции) у тебя может не быть — не опирайся на них, если они прямо не переданы.
1672
1673БИЗНЕС‑ЦЕЛЬ:
1674Максимизировать оборот (выручку и заказы) при соблюдении ограничений:
1675- целевая маржа — не ниже 25% (нормальный диапазон 25–30%; выше 30% — высокая маржа);
1676- целевой ДРР — около 20% (нормальный диапазон 17–25%).
1677- абсолютный минимум маржи, ниже которого НЕЛЬЗЯ предлагать снижать цену — 15%.
1678
1679Допустимы временные отклонения:
1680- при агрессивном разгоне нового товара можно повышать ДРР до ~35%, если прибыль остаётся положительной и маржа не падает ниже 30%;
1681- при огромных остатках (>60 дней запаса) или выводе товара допускается снижать маржу, но всё равно не ниже 15%.
1682
1683ДОПУСТИМЫЕ РЫЧАГИ УПРАВЛЕНИЯ:
1684Ты можешь рекомендовать действия только в четырёх областях:
16851. Цена.
16862. Реклама / ДРР (ставки, бюджеты).
16873. Остатки и логистика (объём и распределение остатков, поставки, влияние на срок доставки).
16884. Карточка товара (фото, заголовок, описание).
1689
1690Запрещено предлагать любые действия вне этих четырёх областей.
1691
1692ПОРОГИ И ОПРЕДЕЛЕНИЯ СОСТОЯНИЙ:
1693
1694Время доставки:
1695- Нормальное: ≤ 35 часов.
1696- Плохое: > 35 и ≤ 40 часов, особенно если выросло к прошлому периоду.
1697- Критически плохое: > 40 часов.
1698
1699Остатки / «хватит на дней»:
1700- Критически мало: ≤ 7 дней.
1701- Низкий запас: > 7 и ≤ 14 дней.
1702- Нормально: > 14 и ≤ 30 дней.
1703- Избыточный запас: > 30 дней.
1704- Огромные остатки: > 60 дней.
1705
1706Маржа (прибыль в %):
1707- Очень низкая: < 20%.
1708- Низкая: 20–25%.
1709- Нормальная: 25–30%.
1710- Высокая: > 30%.
1711- Абсолютный минимум, ниже которого нельзя снижать цену — 15%.
1712
1713ДРР:
1714- Целевой ДРР: ~20%.
1715- Слишком низкий ДРР: < 17% (есть запас для роста трафика).
1716- Нормальный ДРР: 17–25%.
1717- Слишком высокий ДРР: > 25% (режет прибыль; допустим только как временная мера при разгоне нового товара).
1718
1719ШАГИ ИЗМЕНЕНИЯ ЦЕНЫ:
1720Разовые рекомендации по изменению цены (в любую сторону) должны быть в диапазоне примерно 5–10%, не больше. Нельзя предлагать очень резких скачков цены за один шаг.
1721
1722ПРИОРИТЕТЫ АНАЛИЗА:
1723
17241. Остатки и время доставки — первый приоритет.
17252. Потом — выручка, заказы, прибыль, маржа и ДРР.
17263. Затем — показы, CTR, CR, CRL, конверсия и качество карточки.
1727
1728ПРАВИЛА АНАЛИЗА:
1729
17301. ОСТАТКИ И ВРЕМЯ ДОСТАВКИ — ГЛАВНЫЕ СТОП‑ФАКТОРЫ
1731
1732Если остатки критически низкие или низкие (≤14 дней) ИЛИ время доставки плохое/критическое (>35 часов):
1733
1734- Главная задача — не уйти в out of stock и восстановить нормальную доставку.
1735- Рекомендовать в первую очередь:
1736 • [Остатки] Поставку товара и/или перераспределение на ближайший склад, чтобы довести запас минимум до 14–30 дней и сократить срок доставки.
1737 • [Цена] При необходимости временно ПОВЫСИТЬ цену (на 5–10%), чтобы притормозить продажи, пока товар едет или пока не нормализуется доставка.
1738 • [Реклама] Временно СНИЗИТЬ ДРР (урезать ставки и бюджеты, отключить самые неэффективные кампании/ключи), чтобы не разгонять спрос.
1739
1740ВАЖНО: В этом состоянии СТРОГО ЗАПРЕЩЕНО:
1741- советовать снижение цены,
1742- советовать повышение ДРР,
1743даже если упали показы, клики или заказы.
1744
17452. НОРМАЛЬНЫЕ ОСТАТКИ И ДОСТАВКА
1746
1747Если остатки >14 дней и время доставки ≤35 часов (нормальные):
1748
1749- Можно фокусироваться на росте оборта и оптимизации прибыли в рамках целевых маржи и ДРР.
1750- При избыточных остатках:
1751 • 30–60 дней: можно аккуратно усиливать спрос (повышать ДРР, немного снижать цену, но по возможности держать маржу ≥25% и не опускаться ниже 20%).
1752 • >60 дней (огромные остатки): допусти более агрессивный разгон (снижение цены, повышение ДРР), но маржа всё равно не должна опускаться ниже 15%.
1753
17543. ВЫРУЧКА, ЗАКАЗЫ, ПРИБЫЛЬ, МАРЖА
1755
1756- Если выручка и заказы падают при нормальных остатках и доставке:
1757 • искать причины в показах, рекламе, цене, конверсии и качестве карточки;
1758 • смотреть динамику CTR/CR/CRL.
1759
1760- Если прибыль и маржа низкие (ниже 25%) или падают, особенно на фоне высокого ДРР (>25%):
1761 • основная цель — вернуть маржу и прибыль к целевым уровням;
1762 • рекомендовать снижать ДРР (резать ставки и бюджеты, отключать неэффективные кампании/ключи);
1763 • рассматривать повышение цены (в разумных пределах 5–10%), если спрос это позволяет;
1764 • не предлагать существенное снижение цены, если маржа уже низкая или близка к минимуму (особенно ниже 20%).
1765
17664. РЕКЛАМА И ДРР
1767
1768- Высокий ДРР (>25%):
1769 • реклама неэффективна или слишком агрессивна;
1770 • рекомендуемые действия:
1771 – [Реклама] Снизить ДРР: уменьшить ставки и бюджеты, отключить слабые кампании/запросы;
1772 – при нормальных остатках и низком CR дополнительно: немного снизить цену и/или улучшить карточку, чтобы поднять конверсию;
1773 – исключение — новый товар: временно можно терпеть ДРР до ~35%, но с контролем маржи (не ниже 15–20%).
1774
1775- Слишком низкий ДРР (<17%) при нормальной/высокой марже и достатенных остатках:
1776 • есть потенциал роста оборта;
1777 • рекомендовать:
1778 – [Реклама] Постепенно повышать ДРР: поднимать ставки, расширять кампании;
1779 – при необходимости можно немного поднять цену, чтобы сохранить маржу при росте трафика.
1780
1781- Если показы упали, а остатки и доставка в норме:
1782 • в приоритете рекламные действия:
1783 – [Реклама] Повысить ДРР для ускорения продаж (если ДРР низкий);
1784 • вторым шагом:
1785 – [Цена] Немного снизить цену (5–10%), если маржа выше целевой;
1786 – [Карточка] При необходимости доработать заголовок и ключевые слова для релевантности.
1787
17886. ПОЛОЖИТЕЛЬНАЯ ДИНАМИКА
1789
1790Если выручка, заказы и прибыль растут, маржа ≥25%, ДРР в норме (17–25%), остатки достаточные (>14 дней) и нет риска out of stock, срок доставки хороший (≤35 часов):
1791
1792- не предлагать резких изменений;
1793- основная рекомендация:
1794 • [Цена] Осторожно тестировать повышение цены на 5–10% с контролем CR и выручки;
1795 • [Реклама] При низком ДРР и хорошем запасе по прибыли — мягко расширять рекламу.
1796
17977. НОВЫЕ ТОВАРЫ И ВЫВОД ТОВАРА
1798
1799- Новый товар (распознавать по очень малому количеству заказов за период при нормальных/избыточных остатках):
1800 • допустимо:
1801 – [Реклама] Повышать ДРР до ~35% ради разгона, если есть запас по марже и бюджу;
1802 – [Цена] Сильных скидок не давать сразу, чтобы не убивать маржу; тестировать цену аккуратно.
1803
1804- Товар под вывод / очень большие остатки (запас >60 дней и слабые продажи):
1805 • КРИТИЧЕСКИ ВАЖНО: Перед любой рекомендацией по снижению цены ОБЯЗАТЕЛЬНО проверь текущую маржу:
1806 – Если маржа <20% (низкая или очень низкая): СТРОГО ЗАПРЕЩЕНО рекомендовать снижение цены в любом виде.
1807 * Вместо этого рекомендуй:
1808 [Реклама] Повысить ДРР для ускорения продаж (если ДРР низкий);
1809 [Карточка] Улучшить карточку товара для повышения конверсии;
1810 [Остатки] Рассмотреть перераспределение остатков или вывод товара из ассортимента.
1811 – Если маржа ≥20% и <25%: можно осторожно рекомендовать снижение цены на 3-5%, но с обязательным контролем, чтобы маржа не упала ниже 15%.
1812 – Если маржа ≥25%: можно рекомендовать снижение цены на 5-10%, но следить, чтобы маржа не упала ниже 20%.
1813 • ЗАПРЕЩЕНО: Рекомендовать снижение цены без явной проверки и упоминания текущей маржи в рекомендации.
1814 • допустимо:
1815 – [Реклама] Поддерживать или немного повышать ДРР, если это экономически оправдано;
1816 – цель — ускорить оборачиваемость без ухода в ноль или минус по прибыли.
1817
1818ФОРМАТ ОТВЕТА:
1819
18201. Блок «Диагноз» — 1–3 коротких предложения:
1821 - что происходит с товаром (рост/падение выручки, заказов, прибыли);
1822 - в чём основная причина (остатки, реклама, цена, карточка, доставка, конверсия).
1823
18242. Блок «Рекомендации» — маркированный список из 3–7 пунктов.
1825
1826Каждый пункт рекомендаций должен начинаться с области в квадратных скобках: [Цена] / [Реклама] / [Остатки] / [Карточка], а дальше — конкретное действие и короткое объяснение «зачем».
1827
1828Примеры формата:
1829- [Остатки] Сделать поставку на ближайший склад на 2–3 недели продаж, чтобы сократить срок доставки и не уйти в out of stock.
1830- [Реклама] Снизить ДРР с ~25% до ~18–20%, отключив неэффективные кампании/запросы — это повысит маржу и прибыль.
1831- [Цена] Тестово поднять цену на 5–10%, так как показатели растут и есть запас по конверсии.
1832- [Карточка] Переработать первое фото и заголовок, чтобы повысить CTR и вернуть посещаемость карточки.
1833
1834ОГРАНИЧЕНИЯ:
1835- Не перечисляй все возможные варианты действий — выбирай только те, которые реально вытекают из данных и не противоречат друг другу.
1836- Если информации не хватает для однозначного вывода — напиши, какие есть возможные причины и дай для каждой гипотезы отдельный набор действий.
1837- Пиши простым, понятным языком, без сложных терминов и «воды».
1838
1839--------------------
1840ДАННЫЕ ТОВАРА
1841--------------------
1842
1843Товар: ${product.name}
1844Артикул: ${product.article}
1845
1846Метрики:
1847- Выручка: ${product.revenue || 'н/д'} ₽ (изменение: ${product.revenueChange || 0}%)
1848- Прибыль: ${product.profit || 'н/д'} ₽ (${product.profitPercent || 'н/д'}% от выручки)
1849- Показы всего: ${product.impressions || 'н/д'} (изменение: ${product.impressionsChange || 0}%)
1850- Посещения карточки: ${product.cardVisits || 'н/д'} (изменение: ${product.cardVisitsChange || 0}%)
1851- CTR (каталог→карточка): ${product.conversionCatalogToCard || 'н/д'}% (изменение: ${product.conversionCatalogToCardChange || 0}%)
1852- Добавления в корзину: ${product.cartAdditions || 'н/д'} (изменение: ${product.cartAdditionsChange || 0}%)
1853- CRL (карточка→корзина): ${product.conversionCardToCart || 'н/д'}% (изменение: ${product.conversionCardToCartChange || 0}%)
1854- Заказы: ${product.orders || 'н/д'} шт (изменение: ${product.ordersChange || 0}%)
1855- CR (заказы/карточка): ${product.cr || 'н/д'}%
1856- ДРР: ${product.drr || 'н/д'}% (изменение: ${product.drrChange || 0}%)
1857- Средняя цена: ${product.avgPrice || 'н/д'} ₽ (изменение: ${product.avgPriceChange || 0}%)
1858- Остаток: ${product.stock || 'н/д'} шт
1859- На дней: ${daysOfStock || 'н/д'} дней
1860- Доставка: ${product.deliveryTime || 'н/д'}
1861
1862 `;
1863
1864
1865 const response = await RM.aiCall(prompt, {
1866
1867 type: 'json_schema',
1868
1869 json_schema: {
1870
1871 name: 'product_analysis',
1872
1873 schema: {
1874
1875 type: 'object',
1876
1877 properties: {
1878
1879 priority: {
1880
1881 type: 'string',
1882
1883 enum: ['critical', 'high', 'medium', 'low']
1884
1885 },
1886
1887 problems: {
1888
1889 type: 'array',
1890
1891 items: {
1892
1893 type: 'object',
1894
1895 properties: {
1896
1897 type: { type: 'string' },
1898
1899 description: { type: 'string' }
1900
1901 },
1902
1903 required: ['type', 'description']
1904
1905 }
1906
1907 },
1908
1909 recommendations: {
1910
1911 type: 'array',
1912
1913 items: { type: 'string' }
1914
1915 }
1916
1917 },
1918
1919 required: ['priority', 'problems', 'recommendations']
1920
1921 }
1922
1923 }
1924
1925 });
1926
1927
1928 return {
1929
1930 ...response,
1931
1932 daysOfStock,
1933
1934 isLowStock,
1935
1936 isHighDRR,
1937
1938 isLowDRR,
1939
1940 isGrowth,
1941
1942 isLowImpressions,
1943
1944 isLowCR,
1945
1946 isLowProfit,
1947
1948 isBadDeliveryTime,
1949
1950 isOutOfStock
1951
1952 };
1953
1954 } catch (error) {
1955
1956 console.error('Ошибка AI анализа:', error);
1957
1958 return this.basicAnalysis(product, avgMetrics);
1959
1960 }
1961
1962 }
1963
1964
1965 // Определение роста на основе средних показателей
1966
1967 detectGrowth(product, avgMetrics) {
1968
1969 const threshold = 15; // Порог отклонения от среднего в %
1970
1971
1972
1973 // Если выручка растет значительно выше среднего
1974
1975 if (product.revenueChange !== null &&
1976
1977 product.revenueChange > avgMetrics.revenueChange + threshold) {
1978
1979 return true;
1980
1981 }
1982
1983
1984
1985 // Если заказы растут значительно выше среднего
1986
1987 if (product.ordersChange !== null &&
1988
1989 product.ordersChange > avgMetrics.ordersChange + threshold) {
1990
1991 return true;
1992
1993 }
1994
1995
1996
1997 return false;
1998
1999 }
2000
2001 }
2002
2003
2004 // Класс для UI
2005
2006 class AnalyticsUI {
2007
2008 constructor() {
2009
2010 this.container = null;
2011
2012 this.filteredProducts = [];
2013
2014 this.allProducts = [];
2015
2016 this.currentFilter = 'all';
2017
2018 this.isCollapsed = false;
2019
2020 this.isDragging = false;
2021
2022 this.isResizing = false;
2023
2024 this.dragStartX = 0;
2025
2026 this.dragStartY = 0;
2027
2028 this.containerStartX = 0;
2029
2030 this.containerStartY = 0;
2031
2032 this.resizeStartWidth = 0;
2033
2034 this.resizeStartHeight = 0;
2035
2036 this.useAI = true; // По умолчанию AI включен
2037
2038 }
2039
2040
2041 createUI() {
2042
2043 console.log('🎨 Создаем UI...');
2044
2045
2046 // Создаем контейнер для нашего UI
2047
2048 this.container = document.createElement('div');
2049
2050 this.container.id = 'ozon-ai-analytics';
2051
2052 this.container.style.cssText = `
2053
2054 position: fixed;
2055
2056 top: 80px;
2057
2058 right: 20px;
2059
2060 width: 500px;
2061
2062 max-height: 85vh;
2063
2064 background: white;
2065
2066 border-radius: 12px;
2067
2068 box-shadow: 0 4px 20px rgba(0,0,0,0.15);
2069
2070 z-index: 10000;
2071
2072 overflow: hidden;
2073
2074 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2075
2076 resize: both;
2077
2078 min-width: 400px;
2079
2080 min-height: 200px;
2081
2082 `;
2083
2084
2085 // Заголовок (с возможностью перетаскивания)
2086
2087 const header = document.createElement('div');
2088
2089 header.id = 'ozon-ai-header';
2090
2091 header.style.cssText = `
2092
2093 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2094
2095 color: white;
2096
2097 padding: 18px 24px;
2098
2099 font-weight: 600;
2100
2101 font-size: 18px;
2102
2103 display: flex;
2104
2105 justify-content: space-between;
2106
2107 align-items: center;
2108
2109 cursor: move;
2110
2111 user-select: none;
2112
2113 `;
2114
2115
2116 header.innerHTML = `
2117 <span>🤖 AI Аналитик Ozon</span>
2118 <div style="display: flex; gap: 8px;">
2119 <button id="ozon-ai-collapse" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center;">−</button>
2120 <button id="ozon-ai-close" style="background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center;">×</button>
2121 </div>
2122 `;
2123
2124
2125 // Кнопка запуска анализа
2126
2127 const startButton = document.createElement('button');
2128
2129 startButton.id = 'ozon-ai-start';
2130
2131 startButton.textContent = '🚀 Запустить анализ';
2132
2133 startButton.style.cssText = `
2134
2135 width: calc(100% - 40px);
2136
2137 margin: 20px;
2138
2139 padding: 16px;
2140
2141 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2142
2143 color: white;
2144
2145 border: none;
2146
2147 border-radius: 8px;
2148
2149 font-size: 16px;
2150
2151 font-weight: 600;
2152
2153 cursor: pointer;
2154
2155 transition: transform 0.2s;
2156
2157 `;
2158
2159 startButton.onmouseover = () => startButton.style.transform = 'scale(1.02)';
2160
2161 startButton.onmouseout = () => startButton.style.transform = 'scale(1)';
2162
2163
2164 // Переключатель AI анализа
2165 const aiToggleContainer = document.createElement('div');
2166 aiToggleContainer.style.cssText = `
2167 padding: 0 20px 10px 20px;
2168 display: flex;
2169 align-items: center;
2170 justify-content: space-between;
2171 background: #f8f9fa;
2172 margin: 0 20px;
2173 border-radius: 8px;
2174 margin-bottom: 10px;
2175 `;
2176
2177 const aiToggleLabel = document.createElement('label');
2178 aiToggleLabel.style.cssText = `
2179 display: flex;
2180 align-items: center;
2181 gap: 10px;
2182 cursor: pointer;
2183 user-select: none;
2184 padding: 12px 0;
2185 `;
2186
2187 const aiToggleText = document.createElement('span');
2188 aiToggleText.textContent = '🤖 AI анализ';
2189 aiToggleText.style.cssText = `
2190 font-size: 14px;
2191 font-weight: 500;
2192 color: #2c3e50;
2193 `;
2194
2195 const aiToggleSwitch = document.createElement('div');
2196 aiToggleSwitch.id = 'ozon-ai-toggle';
2197 aiToggleSwitch.style.cssText = `
2198 position: relative;
2199 width: 50px;
2200 height: 26px;
2201 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2202 border-radius: 13px;
2203 transition: background 0.3s;
2204 cursor: pointer;
2205 `;
2206
2207 const aiToggleCircle = document.createElement('div');
2208 aiToggleCircle.style.cssText = `
2209 position: absolute;
2210 top: 3px;
2211 left: 3px;
2212 width: 20px;
2213 height: 20px;
2214 background: white;
2215 border-radius: 50%;
2216 transition: transform 0.3s;
2217 transform: translateX(24px);
2218 `;
2219
2220 const aiToggleStatus = document.createElement('span');
2221 aiToggleStatus.id = 'ozon-ai-status';
2222 aiToggleStatus.textContent = 'Включен';
2223 aiToggleStatus.style.cssText = `
2224 font-size: 12px;
2225 color: #27ae60;
2226 font-weight: 600;
2227 `;
2228
2229 aiToggleSwitch.appendChild(aiToggleCircle);
2230 aiToggleLabel.appendChild(aiToggleText);
2231 aiToggleLabel.appendChild(aiToggleSwitch);
2232 aiToggleContainer.appendChild(aiToggleLabel);
2233 aiToggleContainer.appendChild(aiToggleStatus);
2234
2235
2236 // Контейнер для контента
2237
2238 const content = document.createElement('div');
2239
2240 content.id = 'ozon-ai-content';
2241
2242 content.style.cssText = `
2243
2244 padding: 20px;
2245
2246 max-height: calc(85vh - 140px);
2247
2248 overflow-y: auto;
2249
2250 `;
2251
2252
2253 // Индикатор изменения размера
2254
2255 const resizeHandle = document.createElement('div');
2256
2257 resizeHandle.id = 'ozon-ai-resize';
2258
2259 resizeHandle.style.cssText = `
2260
2261 position: absolute;
2262
2263 bottom: 0;
2264
2265 right: 0;
2266
2267 width: 20px;
2268
2269 height: 20px;
2270
2271 cursor: nwse-resize;
2272
2273 background: linear-gradient(135deg, transparent 0%, transparent 50%, #667eea 50%, #667eea 100%);
2274
2275 border-bottom-right-radius: 12px;
2276
2277 `;
2278
2279
2280 this.container.appendChild(header);
2281
2282 this.container.appendChild(startButton);
2283
2284 this.container.appendChild(aiToggleContainer);
2285
2286 this.container.appendChild(content);
2287
2288 this.container.appendChild(resizeHandle);
2289
2290
2291 document.body.appendChild(this.container);
2292
2293
2294 // События для перетаскивания
2295
2296 header.addEventListener('mousedown', (e) => this.startDragging(e));
2297
2298 document.addEventListener('mousemove', (e) => this.drag(e));
2299
2300 document.addEventListener('mouseup', () => this.stopDragging());
2301
2302
2303 // События для изменения размера
2304
2305 resizeHandle.addEventListener('mousedown', (e) => this.startResizing(e));
2306
2307
2308 // События кнопок
2309
2310 document.getElementById('ozon-ai-close').addEventListener('click', () => {
2311
2312 this.container.style.display = 'none';
2313
2314 });
2315
2316
2317 document.getElementById('ozon-ai-collapse').addEventListener('click', () => {
2318
2319 this.toggleCollapse();
2320
2321 });
2322
2323
2324 document.getElementById('ozon-ai-start').addEventListener('click', () => {
2325
2326 this.startAnalysis();
2327
2328 });
2329
2330
2331 // Обработчик переключателя AI
2332 aiToggleSwitch.addEventListener('click', () => {
2333 this.useAI = !this.useAI;
2334
2335 if (this.useAI) {
2336 aiToggleSwitch.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
2337 aiToggleCircle.style.transform = 'translateX(24px)';
2338 aiToggleStatus.textContent = 'Включен';
2339 aiToggleStatus.style.color = '#27ae60';
2340 console.log('✅ AI анализ включен');
2341 } else {
2342 aiToggleSwitch.style.background = '#95a5a6';
2343 aiToggleCircle.style.transform = 'translateX(0)';
2344 aiToggleStatus.textContent = 'Выключен';
2345 aiToggleStatus.style.color = '#e74c3c';
2346 console.log('⚠️ AI анализ выключен - будет использован базовый анализ');
2347 }
2348 });
2349
2350
2351 console.log('✅ UI создан');
2352
2353 }
2354
2355
2356 startDragging(e) {
2357
2358 if (e.target.closest('button')) return; // Не перетаскиваем при клике на кнопки
2359
2360 this.isDragging = true;
2361
2362 this.dragStartX = e.clientX;
2363
2364 this.dragStartY = e.clientY;
2365
2366 const rect = this.container.getBoundingClientRect();
2367
2368 this.containerStartX = rect.left;
2369
2370 this.containerStartY = rect.top;
2371
2372 this.container.style.transition = 'none';
2373
2374 }
2375
2376
2377 drag(e) {
2378
2379 if (this.isDragging) {
2380
2381 const deltaX = e.clientX - this.dragStartX;
2382
2383 const deltaY = e.clientY - this.dragStartY;
2384
2385 this.container.style.left = `${this.containerStartX + deltaX}px`;
2386
2387 this.container.style.top = `${this.containerStartY + deltaY}px`;
2388
2389 this.container.style.right = 'auto';
2390
2391 } else if (this.isResizing) {
2392
2393 const deltaX = e.clientX - this.dragStartX;
2394
2395 const deltaY = e.clientY - this.dragStartY;
2396
2397 const newWidth = Math.max(400, this.resizeStartWidth + deltaX);
2398
2399 const newHeight = Math.max(200, this.resizeStartHeight + deltaY);
2400
2401 this.container.style.width = `${newWidth}px`;
2402
2403 this.container.style.maxHeight = `${newHeight}px`;
2404
2405 }
2406
2407 }
2408
2409
2410 stopDragging() {
2411
2412 this.isDragging = false;
2413
2414 this.isResizing = false;
2415
2416 this.container.style.transition = '';
2417
2418 }
2419
2420
2421 startResizing(e) {
2422
2423 e.stopPropagation();
2424
2425 this.isResizing = true;
2426
2427 this.dragStartX = e.clientX;
2428
2429 this.dragStartY = e.clientY;
2430
2431 this.resizeStartWidth = this.container.offsetWidth;
2432
2433 this.resizeStartHeight = this.container.offsetHeight;
2434
2435 }
2436
2437
2438 toggleCollapse() {
2439
2440 this.isCollapsed = !this.isCollapsed;
2441
2442 const content = document.getElementById('ozon-ai-content');
2443
2444 const startButton = document.getElementById('ozon-ai-start');
2445
2446 const resizeHandle = document.getElementById('ozon-ai-resize');
2447
2448 const collapseButton = document.getElementById('ozon-ai-collapse');
2449
2450
2451
2452 if (this.isCollapsed) {
2453
2454 content.style.display = 'none';
2455
2456 startButton.style.display = 'none';
2457
2458 resizeHandle.style.display = 'none';
2459
2460 collapseButton.textContent = '+';
2461
2462 this.container.style.maxHeight = 'auto';
2463
2464 } else {
2465
2466 content.style.display = 'block';
2467
2468 startButton.style.display = 'block';
2469
2470 resizeHandle.style.display = 'block';
2471
2472 collapseButton.textContent = '−';
2473
2474 this.container.style.maxHeight = '85vh';
2475
2476 }
2477
2478 }
2479
2480
2481 async startAnalysis() {
2482
2483 const content = document.getElementById('ozon-ai-content');
2484
2485 const startButton = document.getElementById('ozon-ai-start');
2486
2487
2488
2489 startButton.disabled = true;
2490
2491 startButton.textContent = '⏳ Загрузка товаров...';
2492
2493
2494 try {
2495
2496 // Шаг 1: Загрузка всех товаров
2497
2498 const collector = new ProductDataCollector();
2499
2500 await collector.loadAllProducts();
2501
2502
2503 startButton.textContent = '📊 Сбор данных...';
2504
2505
2506
2507 // Шаг 2: Сбор данных
2508
2509 const products = collector.collectProductData();
2510
2511
2512 if (products.length === 0) {
2513
2514 content.innerHTML = '<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Не удалось найти товары. Убедитесь, что вы на странице аналитики.</p>';
2515
2516 startButton.disabled = false;
2517
2518 startButton.textContent = '🚀 Запустить анализ';
2519
2520 return;
2521
2522 }
2523
2524
2525 // Шаг 3: AI анализ с прогрессом
2526
2527 const analyzer = new AIAnalyzer();
2528
2529 const onProgress = (current, total, percentage, remaining) => {
2530 const remainingText = remaining > 0 ? ` (~${remaining} сек)` : '';
2531 startButton.textContent = `🤖 AI анализ: ${current}/${total} (${percentage}%)${remainingText}`;
2532 };
2533
2534 // Передаем флаг useAI в анализатор
2535 const analyzedProducts = this.useAI
2536 ? await analyzer.analyzeProducts(products, onProgress)
2537 : await analyzer.analyzeProductsBasic(products, onProgress);
2538
2539
2540 this.allProducts = analyzedProducts;
2541
2542 this.filteredProducts = analyzedProducts;
2543
2544
2545 // Вычисляем общую выручку и прибыль
2546
2547 const totalRevenue = analyzedProducts.reduce((sum, p) => sum + (p.revenue || 0), 0);
2548
2549 const totalProfit = analyzedProducts.reduce((sum, p) => sum + (p.profit || 0), 0);
2550
2551 const totalOrders = analyzedProducts.reduce((sum, p) => sum + (p.orders || 0), 0);
2552
2553
2554
2555 // Вычисляем средний ДРР (взвешенный по выручке)
2556
2557 let totalDrrWeighted = 0;
2558
2559 let totalRevenueForDrr = 0;
2560
2561 analyzedProducts.forEach(p => {
2562
2563 if (p.drr !== null && p.revenue) {
2564
2565 totalDrrWeighted += p.drr * p.revenue;
2566
2567 totalRevenueForDrr += p.revenue;
2568
2569 }
2570
2571 });
2572
2573 const avgDrr = totalRevenueForDrr > 0 ? totalDrrWeighted / totalRevenueForDrr : 0;
2574
2575
2576 // Шаг 4: Отображение результатов
2577
2578 this.displayResults(analyzedProducts, {
2579
2580 totalRevenue,
2581
2582 totalProfit,
2583
2584 totalOrders,
2585
2586 avgDrr
2587
2588 });
2589
2590
2591 startButton.textContent = '🔄 Анализировать снова';
2592
2593 startButton.disabled = false;
2594
2595
2596 } catch (error) {
2597
2598 console.error('Ошибка анализа:', error);
2599
2600 content.innerHTML = `<p style="color: #e74c3c; padding: 20px; text-align: center; font-size: 14px;">❌ Ошибка: ${error.message}</p>`;
2601
2602 startButton.disabled = false;
2603
2604 startButton.textContent = '🚀 Запустить анализ';
2605
2606 }
2607
2608 }
2609
2610
2611 displayResults(products, totals) {
2612
2613 const content = document.getElementById('ozon-ai-content');
2614
2615
2616
2617 // Блок с общими показателями
2618
2619 const totalSalesBlock = this.createTotalSalesBlock(totals);
2620
2621
2622
2623 // Фильтры
2624
2625 const filters = this.createFilters(products);
2626
2627
2628
2629 // Список товаров
2630
2631 const productsList = this.createProductsList(products);
2632
2633 content.innerHTML = '';
2634
2635 content.appendChild(totalSalesBlock);
2636
2637 content.appendChild(filters);
2638
2639 content.appendChild(productsList);
2640
2641 }
2642
2643
2644 createTotalSalesBlock(totals) {
2645
2646 const block = document.createElement('div');
2647
2648 block.id = 'ozon-ai-total-sales';
2649
2650 block.style.cssText = `
2651
2652 margin-bottom: 20px;
2653
2654 padding: 16px;
2655
2656 background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
2657
2658 border-radius: 8px;
2659
2660 `;
2661
2662
2663
2664 const profitColor = totals.totalProfit >= 0 ? '#27ae60' : '#e74c3c';
2665
2666 const profitPercent = totals.totalRevenue > 0 ? ((totals.totalProfit / totals.totalRevenue) * 100).toFixed(1) : 0;
2667
2668 const profitPercentColor = profitPercent >= 25 ? '#27ae60' : '#e74c3c';
2669
2670
2671
2672 block.innerHTML = `
2673
2674 <div style="font-size: 14px; font-weight: 600; color: #2c3e50; margin-bottom: 12px;">📊 Общие показатели</div>
2675
2676 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
2677
2678 <div style="background: white; padding: 10px; border-radius: 6px;">
2679
2680 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Общая выручка</div>
2681
2682 <div style="font-weight: 600; color: #2c3e50;">${totals.totalRevenue.toLocaleString()} ₽</div>
2683
2684 </div>
2685
2686 <div style="background: white; padding: 10px; border-radius: 6px;">
2687
2688 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Общая прибыль</div>
2689
2690 <div style="font-weight: 600; color: ${profitColor};">${totals.totalProfit.toLocaleString()} ₽ <span style="color: ${profitPercentColor};">(${profitPercent}%)</span></div>
2691
2692 </div>
2693
2694 <div style="background: white; padding: 10px; border-radius: 6px;">
2695
2696 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Всего заказов</div>
2697
2698 <div style="font-weight: 600; color: #2c3e50;">${totals.totalOrders.toLocaleString()}</div>
2699
2700 </div>
2701
2702 <div style="background: white; padding: 10px; border-radius: 6px;">
2703
2704 <div style="color: #7f8c8d; font-size: 11px; margin-bottom: 4px;">Средний ДРР</div>
2705
2706 <div style="font-weight: 600; color: #2c3e50;">${totals.avgDrr.toFixed(1)}%</div>
2707
2708 </div>
2709
2710 </div>
2711
2712 `;
2713
2714
2715
2716 return block;
2717
2718 }
2719
2720
2721 // Фильтры
2722
2723 createFilters(products) {
2724
2725 const filtersContainer = document.createElement('div');
2726
2727 filtersContainer.style.cssText = `
2728
2729 margin-bottom: 20px;
2730
2731 `;
2732
2733
2734 // Поле поиска
2735
2736 const searchContainer = document.createElement('div');
2737
2738 searchContainer.style.cssText = `
2739
2740 margin-bottom: 12px;
2741
2742 `;
2743
2744
2745
2746 const searchInput = document.createElement('input');
2747
2748 searchInput.type = 'text';
2749
2750 searchInput.placeholder = '🔍 Поиск по названию или артикулу...';
2751
2752 searchInput.id = 'ozon-ai-search';
2753
2754 searchInput.style.cssText = `
2755
2756 width: 100%;
2757
2758 padding: 10px 12px;
2759
2760 border: 2px solid #ecf0f1;
2761
2762 border-radius: 6px;
2763
2764 font-size: 14px;
2765
2766 font-family: inherit;
2767
2768 outline: none;
2769
2770 transition: border-color 0.2s;
2771
2772 `;
2773
2774
2775
2776 searchInput.addEventListener('focus', () => {
2777
2778 searchInput.style.borderColor = '#667eea';
2779
2780 });
2781
2782
2783
2784 searchInput.addEventListener('blur', () => {
2785
2786 searchInput.style.borderColor = '#ecf0f1';
2787
2788 });
2789
2790
2791
2792 searchInput.addEventListener('input', (e) => {
2793
2794 this.applySearch(e.target.value);
2795
2796 });
2797
2798
2799
2800 searchContainer.appendChild(searchInput);
2801
2802 filtersContainer.appendChild(searchContainer);
2803
2804
2805 // Кнопки фильтров
2806
2807 const buttonsContainer = document.createElement('div');
2808
2809 buttonsContainer.id = 'ozon-ai-filter-buttons';
2810
2811 buttonsContainer.style.cssText = `
2812
2813 display: flex;
2814
2815 flex-wrap: wrap;
2816
2817 gap: 8px;
2818
2819 `;
2820
2821
2822 // Подсчет товаров по категориям
2823
2824 const critical = products.filter(p => p.analysis.priority === 'critical').length;
2825
2826 const high = products.filter(p => p.analysis.priority === 'high').length;
2827
2828 const outOfStock = products.filter(p => p.analysis.isOutOfStock).length;
2829
2830 const lowStock = products.filter(p => p.analysis.isLowStock).length;
2831
2832 const highDRR = products.filter(p => p.analysis.isHighDRR).length;
2833
2834 const lowDRR = products.filter(p => p.analysis.isLowDRR).length;
2835
2836 const growth = products.filter(p => p.analysis.isGrowth).length;
2837
2838 const lowImpressions = products.filter(p => p.analysis.isLowImpressions).length;
2839
2840 const lowCR = products.filter(p => p.analysis.isLowCR).length;
2841
2842 const lowProfit = products.filter(p => p.analysis.isLowProfit).length;
2843
2844 const badDeliveryTime = products.filter(p => p.analysis.isBadDeliveryTime).length;
2845
2846
2847 const filterButtons = [
2848
2849 { id: 'all', label: `Все (${products.length})`, color: '#95a5a6' },
2850
2851 { id: 'critical', label: `🔴 Критичные (${critical})`, color: '#e74c3c' },
2852
2853 { id: 'high', label: `🟠 Высокий (${high})`, color: '#f39c12' },
2854
2855 { id: 'outOfStock', label: `🚨 Out of Stock (${outOfStock})`, color: '#c0392b' },
2856
2857 { id: 'lowStock', label: `📦 Низкие остатки (${lowStock})`, color: '#e67e22' },
2858
2859 { id: 'highDRR', label: `💰 Высокий ДРР (${highDRR})`, color: '#c0392b' },
2860
2861 { id: 'lowDRR', label: `📊 Повысить ДРР (${lowDRR})`, color: '#16a085' },
2862
2863 { id: 'lowImpressions', label: `📉 Упали показы (${lowImpressions})`, color: '#9b59b6' },
2864
2865 { id: 'lowCR', label: `📊 Упал CR (${lowCR})`, color: '#e91e63' },
2866
2867 { id: 'lowProfit', label: `💸 Низкая прибыль (${lowProfit})`, color: '#d32f2f' },
2868
2869 { id: 'badDeliveryTime', label: `⏱️ Плохое время (${badDeliveryTime})`, color: '#8e44ad' },
2870
2871 { id: 'growth', label: `📈 Рост (${growth})`, color: '#27ae60' }
2872
2873 ];
2874
2875
2876 filterButtons.forEach(filter => {
2877
2878 const btn = document.createElement('button');
2879
2880 btn.textContent = filter.label;
2881
2882 btn.dataset.filterId = filter.id;
2883
2884 btn.style.cssText = `
2885
2886 padding: 8px 12px;
2887
2888 background: ${this.currentFilter === filter.id ? filter.color : '#ecf0f1'};
2889
2890 color: ${this.currentFilter === filter.id ? 'white' : '#2c3e50'};
2891
2892 border: none;
2893
2894 border-radius: 6px;
2895
2896 font-size: 13px;
2897
2898 font-weight: 500;
2899
2900 cursor: pointer;
2901
2902 transition: all 0.2s;
2903
2904 `;
2905
2906
2907
2908 btn.addEventListener('click', () => {
2909
2910 this.currentFilter = filter.id;
2911
2912 this.applyFilter(filter.id);
2913
2914 });
2915
2916
2917 buttonsContainer.appendChild(btn);
2918
2919 });
2920
2921
2922 filtersContainer.appendChild(buttonsContainer);
2923
2924 return filtersContainer;
2925
2926 }
2927
2928
2929 applySearch(searchTerm) {
2930
2931 const term = searchTerm.toLowerCase().trim();
2932
2933
2934
2935 console.log(`🔍 Поиск по запросу: "${term}"`);
2936
2937
2938
2939 if (!term) {
2940
2941 // Если поиск пустой, применяем текущий фильтр
2942
2943 this.applyFilter(this.currentFilter);
2944
2945 return;
2946
2947 }
2948
2949
2950
2951 // Фильтруем по поисковому запросу
2952
2953 const filtered = this.allProducts.filter(p => {
2954
2955 const nameMatch = p.name.toLowerCase().includes(term);
2956
2957 const articleMatch = p.article.includes(term);
2958
2959 return nameMatch || articleMatch;
2960
2961 });
2962
2963
2964
2965 console.log(`✅ Найдено товаров: ${filtered.length}`);
2966
2967
2968
2969 // Сортируем по выручке
2970
2971 filtered.sort((a, b) => {
2972
2973 const revenueA = a.revenue || 0;
2974
2975 const revenueB = b.revenue || 0;
2976
2977 return revenueB - revenueA;
2978
2979 });
2980
2981
2982 this.filteredProducts = filtered;
2983
2984
2985
2986 // Обновляем только список товаров, не трогая фильтры
2987
2988 const content = document.getElementById('ozon-ai-content');
2989
2990 const productsList = this.createProductsList(filtered);
2991
2992
2993
2994 // Находим и удаляем только список товаров (третий элемент в content)
2995
2996 const children = content.children;
2997
2998 if (children.length > 2) {
2999
3000 children[2].remove();
3001
3002 }
3003
3004
3005
3006 content.appendChild(productsList);
3007
3008 }
3009
3010
3011 applyFilter(filterId) {
3012
3013 let filtered = this.allProducts;
3014
3015
3016
3017 console.log(`🔍 Применяем фильтр: ${filterId}`);
3018
3019
3020
3021 // Очищаем поле поиска при смене фильтра
3022
3023 const searchInput = document.getElementById('ozon-ai-search');
3024
3025 if (searchInput) {
3026
3027 searchInput.value = '';
3028
3029 }
3030
3031
3032 switch(filterId) {
3033
3034 case 'critical':
3035
3036 filtered = this.allProducts.filter(p => p.analysis.priority === 'critical');
3037
3038 break;
3039
3040 case 'high':
3041
3042 filtered = this.allProducts.filter(p => p.analysis.priority === 'high');
3043
3044 break;
3045
3046 case 'outOfStock':
3047
3048 filtered = this.allProducts.filter(p => p.analysis.isOutOfStock);
3049
3050 break;
3051
3052 case 'lowStock':
3053
3054 filtered = this.allProducts.filter(p => p.analysis.isLowStock);
3055
3056 break;
3057
3058 case 'highDRR':
3059
3060 filtered = this.allProducts.filter(p => p.analysis.isHighDRR);
3061
3062 break;
3063
3064 case 'lowDRR':
3065
3066 filtered = this.allProducts.filter(p => p.analysis.isLowDRR);
3067
3068 break;
3069
3070 case 'lowImpressions':
3071
3072 filtered = this.allProducts.filter(p => p.analysis.isLowImpressions);
3073
3074 console.log('📊 Товары с упавшими показами:', filtered.map(p => `${p.article} (${p.impressionsChange}%)`));
3075
3076 break;
3077
3078 case 'lowCR':
3079
3080 filtered = this.allProducts.filter(p => p.analysis.isLowCR);
3081
3082 break;
3083
3084 case 'lowProfit':
3085
3086 filtered = this.allProducts.filter(p => p.analysis.isLowProfit);
3087
3088 break;
3089
3090 case 'badDeliveryTime':
3091
3092 filtered = this.allProducts.filter(p => p.analysis.isBadDeliveryTime);
3093
3094 break;
3095
3096 case 'growth':
3097
3098 filtered = this.allProducts.filter(p => p.analysis.isGrowth);
3099
3100 break;
3101
3102 }
3103
3104
3105 console.log(`✅ Найдено товаров: ${filtered.length}`);
3106
3107
3108 // Сортируем по выручке (от большей к меньшей)
3109
3110 filtered.sort((a, b) => {
3111
3112 const revenueA = a.revenue || 0;
3113
3114 const revenueB = b.revenue || 0;
3115
3116 return revenueB - revenueA;
3117
3118 });
3119
3120
3121 this.filteredProducts = filtered;
3122
3123
3124
3125 // Обновляем только список товаров и кнопки фильтров
3126
3127 const content = document.getElementById('ozon-ai-content');
3128
3129 const productsList = this.createProductsList(filtered);
3130
3131
3132
3133 // Находим и удаляем только список товаров (третий элемент в content)
3134
3135 const children = content.children;
3136
3137 if (children.length > 2) {
3138
3139 children[2].remove();
3140
3141 }
3142
3143
3144
3145 content.appendChild(productsList);
3146
3147
3148
3149 // Обновляем стили кнопок фильтров
3150
3151 this.updateFilterButtons(filterId);
3152
3153 }
3154
3155
3156 updateFilterButtons(activeFilterId) {
3157
3158 const buttonsContainer = document.getElementById('ozon-ai-filter-buttons');
3159
3160 if (!buttonsContainer) return;
3161
3162
3163
3164 const buttons = buttonsContainer.querySelectorAll('button');
3165
3166
3167
3168 const filterColors = {
3169
3170 'all': '#95a5a6',
3171
3172 'critical': '#e74c3c',
3173
3174 'high': '#f39c12',
3175
3176 'outOfStock': '#c0392b',
3177
3178 'lowStock': '#e67e22',
3179
3180 'highDRR': '#c0392b',
3181
3182 'lowDRR': '#16a085',
3183
3184 'lowImpressions': '#9b59b6',
3185
3186 'lowCR': '#e91e63',
3187
3188 'lowProfit': '#d32f2f',
3189
3190 'badDeliveryTime': '#8e44ad',
3191
3192 'growth': '#27ae60'
3193
3194 };
3195
3196
3197
3198 buttons.forEach(btn => {
3199
3200 const filterId = btn.dataset.filterId;
3201
3202 if (filterId === activeFilterId) {
3203
3204 btn.style.background = filterColors[filterId];
3205
3206 btn.style.color = 'white';
3207
3208 } else {
3209
3210 btn.style.background = '#ecf0f1';
3211
3212 btn.style.color = '#2c3e50';
3213
3214 }
3215
3216 });
3217
3218 }
3219
3220
3221 createProductsList(products) {
3222
3223 const list = document.createElement('div');
3224
3225 list.style.cssText = `
3226
3227 display: flex;
3228
3229 flex-direction: column;
3230
3231 gap: 12px;
3232
3233 `;
3234
3235
3236 products.forEach(product => {
3237
3238 const card = this.createProductCard(product);
3239
3240 list.appendChild(card);
3241
3242 });
3243
3244
3245 return list;
3246
3247 }
3248
3249
3250 formatMetric(value, change, isPercent = false) {
3251
3252 // Округляем проценты до десятых
3253
3254 let displayValue = value;
3255
3256 if (isPercent && value !== null && value !== undefined) {
3257
3258 displayValue = parseFloat(value.toFixed(1));
3259
3260 }
3261
3262
3263
3264 const valueStr = displayValue !== null && displayValue !== undefined ?
3265
3266 (isPercent ? `${displayValue}%` : displayValue.toLocaleString()) : '—';
3267
3268
3269
3270 if (change === null || change === undefined) return valueStr;
3271
3272
3273
3274 // Округляем изменение до десятых
3275
3276 const roundedChange = parseFloat(change.toFixed(1));
3277
3278 const changeStr = roundedChange > 0 ? `+${roundedChange}%` : `${roundedChange}%`;
3279
3280 const color = roundedChange > 0 ? '#27ae60' : '#e74c3c';
3281
3282
3283
3284 return `${valueStr} <span style="color: ${color}; font-size: 11px;">(${changeStr})</span>`;
3285
3286 }
3287
3288
3289 createProductCard(product) {
3290
3291 const card = document.createElement('div');
3292
3293
3294
3295 const priorityColors = {
3296
3297 critical: '#e74c3c',
3298
3299 high: '#f39c12',
3300
3301 medium: '#3498db',
3302
3303 low: '#95a5a6'
3304
3305 };
3306
3307
3308 const priorityLabels = {
3309
3310 critical: '🔴 Критичный',
3311
3312 high: '🟠 Высокий',
3313
3314 medium: '🟡 Средний',
3315
3316 low: '🟢 Низкий'
3317
3318 };
3319
3320
3321 // Определяем цвет прибыли
3322
3323 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
3324
3325
3326 card.style.cssText = `
3327
3328 background: white;
3329
3330 border: 2px solid ${priorityColors[product.analysis.priority]};
3331
3332 border-radius: 8px;
3333
3334 padding: 14px;
3335
3336 cursor: pointer;
3337
3338 transition: all 0.2s;
3339
3340 `;
3341
3342
3343 card.innerHTML = `
3344
3345 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
3346
3347 <div style="flex: 1;">
3348
3349 <div style="font-weight: 600; font-size: 14px; color: #2c3e50; margin-bottom: 4px;">${product.name}</div>
3350
3351 <div class="article-copy" style="font-size: 12px; color: #7f8c8d; cursor: pointer; user-select: none;" title="Нажмите, чтобы скопировать артикул">Арт. ${product.article}</div>
3352
3353 </div>
3354
3355 <div style="background: ${priorityColors[product.analysis.priority]}; color: white; padding: 5px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; white-space: nowrap;">
3356
3357 ${priorityLabels[product.analysis.priority]}
3358
3359 </div>
3360
3361 </div>
3362
3363
3364 ${product.analysis.problems.length > 0 ? `
3365
3366 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 13px; font-weight: 500; color: #856404;">
3367
3368 ${product.analysis.problems.slice(0, 2).map(p => `
3369
3370 <div style="margin-bottom: 4px;">⚠️ ${p.description}</div>
3371
3372 `).join('')}
3373
3374 </div>
3375
3376 ` : ''}
3377
3378
3379 <!-- БЛОК 1: Выручка / ДРР / Прибыль / Прибыль % / Комиссия / Себестоимость -->
3380 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3381
3382 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">💰 Финансы</div>
3383
3384 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3385
3386 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
3387
3388 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
3389
3390 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</span></div>
3391
3392 <div><strong>Прибыль %:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</span></div>
3393
3394 <div><strong>Комиссия:</strong> ${product.totalCommission !== null ? `${product.totalCommission.toLocaleString()} ₽` : '—'} ${product.totalCommissionPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCommissionPercent}%)</span>` : ''}</div>
3395
3396 <div><strong>Себестоимость:</strong> ${product.totalCost !== null ? `${product.totalCost.toLocaleString()} ₽` : '—'} ${product.totalCostPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCostPercent}%)</span>` : ''}</div>
3397
3398 </div>
3399
3400 </div>
3401
3402
3403 <!-- БЛОК 2: Показы / Посещения карточки / CTR -->
3404 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3405
3406 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">👁️ Трафик</div>
3407
3408 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; font-size: 11px;">
3409
3410 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
3411
3412 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
3413
3414 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
3415
3416 </div>
3417
3418 </div>
3419
3420
3421 <!-- БЛОК 3: Добавления в корзину / CRL / Заказы / CR -->
3422 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 8px;">
3423
3424 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">🛒 Конверсия</div>
3425
3426 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3427
3428 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
3429
3430 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
3431
3432 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
3433
3434 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
3435
3436 </div>
3437
3438 </div>
3439
3440
3441 <!-- БЛОК 4: Остаток / На дней / Время доставки / Цена -->
3442 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px;">
3443
3444 <div style="font-size: 11px; font-weight: 600; color: #2c3e50; margin-bottom: 6px;">📦 Логистика и цена</div>
3445
3446 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
3447
3448 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
3449
3450 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
3451
3452 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
3453
3454 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
3455
3456 </div>
3457
3458 </div>
3459
3460
3461 <div style="font-size: 11px; color: #7f8c8d; border-top: 1px solid #ecf0f1; padding-top: 8px;">
3462
3463 <strong>Рекомендации:</strong>
3464
3465 ${product.analysis.recommendations.slice(0, 1).map(r => `<div>• ${r}</div>`).join('')}
3466
3467 </div>
3468
3469 `;
3470
3471
3472 // Обработчик копирования артикула
3473
3474 const articleElement = card.querySelector('.article-copy');
3475
3476 articleElement.addEventListener('click', async (e) => {
3477
3478 e.stopPropagation();
3479
3480 try {
3481
3482 await GM.setClipboard(product.article);
3483
3484 const originalText = articleElement.textContent;
3485
3486 articleElement.textContent = '✓ Скопировано!';
3487
3488 articleElement.style.color = '#27ae60';
3489
3490 setTimeout(() => {
3491
3492 articleElement.textContent = originalText;
3493
3494 articleElement.style.color = '#7f8c8d';
3495
3496 }, 1500);
3497
3498 } catch (error) {
3499
3500 console.error('Ошибка копирования:', error);
3501
3502 }
3503
3504 });
3505
3506
3507 card.addEventListener('click', () => {
3508
3509 this.showProductDetails(product);
3510
3511 });
3512
3513
3514 return card;
3515
3516 }
3517
3518
3519 showProductDetails(product) {
3520
3521 // Определяем цвет прибыли
3522
3523 const profitColor = product.profitPercent !== null && product.profitPercent >= 25 ? '#27ae60' : '#e74c3c';
3524
3525
3526 // Создаем модальное окно с детальной информацией
3527
3528 const modal = document.createElement('div');
3529
3530 modal.style.cssText = `
3531
3532 position: fixed;
3533
3534 top: 0;
3535
3536 left: 0;
3537
3538 right: 0;
3539
3540 bottom: 0;
3541
3542 background: rgba(0,0,0,0.7);
3543
3544 z-index: 10001;
3545
3546 display: flex;
3547
3548 align-items: center;
3549
3550 justify-content: center;
3551
3552 padding: 20px;
3553
3554 `;
3555
3556
3557 const modalContent = document.createElement('div');
3558
3559 modalContent.style.cssText = `
3560
3561 background: white;
3562
3563 border-radius: 12px;
3564
3565 padding: 24px;
3566
3567 max-width: 700px;
3568
3569 max-height: 80vh;
3570
3571 overflow-y: auto;
3572
3573 width: 100%;
3574
3575 `;
3576
3577
3578 modalContent.innerHTML = `
3579
3580 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px;">
3581
3582 <h2 style="margin: 0; font-size: 18px; color: #2c3e50;">${product.name}</h2>
3583
3584 <button id="close-modal" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #95a5a6;">×</button>
3585
3586 </div>
3587
3588
3589 <div style="font-size: 12px; color: #7f8c8d; margin-bottom: 16px;">Артикул: ${product.article}</div>
3590
3591
3592 <!-- БЛОК 1: Выручка / ДРР / Прибыль / Прибыль % / Комиссия / Себестоимость -->
3593 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3594
3595 💰 Финансы
3596
3597 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3598
3599 <div><strong>Выручка:</strong> ${this.formatMetric(product.revenue, product.revenueChange)} ₽</div>
3600
3601 <div><strong>ДРР:</strong> ${this.formatMetric(product.drr, product.drrChange, true)}</div>
3602
3603 <div><strong>Прибыль:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profit !== null ? `${product.profit.toLocaleString()} ₽` : '—'}</span></div>
3604
3605 <div><strong>Прибыль %:</strong> <span style="color: ${profitColor}; font-weight: 600;">${product.profitPercent !== null ? `${product.profitPercent}%` : '—'}</span></div>
3606
3607 <div><strong>Комиссия:</strong> ${product.totalCommission !== null ? `${product.totalCommission.toLocaleString()} ₽` : '—'} ${product.totalCommissionPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCommissionPercent}%)</span>` : ''}</div>
3608
3609 <div><strong>Себестоимость:</strong> ${product.totalCost !== null ? `${product.totalCost.toLocaleString()} ₽` : '—'} ${product.totalCostPercent !== null ? `<span style="font-size: 10px; color: #7f8c8d;">(${product.totalCostPercent}%)</span>` : ''}</div>
3610
3611 </div>
3612
3613 </div>
3614
3615
3616 <!-- БЛОК 2: Показы / Посещения карточки / CTR -->
3617 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3618
3619 👁️ Трафик
3620
3621 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; font-size: 13px;">
3622
3623 <div><strong>Показы:</strong> ${this.formatMetric(product.impressions, product.impressionsChange)}</div>
3624
3625 <div><strong>Карточка:</strong> ${this.formatMetric(product.cardVisits, product.cardVisitsChange)}</div>
3626
3627 <div><strong>CTR:</strong> ${this.formatMetric(product.conversionCatalogToCard, product.conversionCatalogToCardChange, true)}</div>
3628
3629 </div>
3630
3631 </div>
3632
3633
3634 <!-- БЛОК 3: Добавления в корзину / CRL / Заказы / CR -->
3635 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3636
3637 🛒 Конверсия
3638
3639 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3640
3641 <div><strong>Корзины:</strong> ${this.formatMetric(product.cartAdditions, product.cartAdditionsChange)}</div>
3642
3643 <div><strong>CRL:</strong> ${this.formatMetric(product.conversionCardToCart, product.conversionCardToCartChange, true)}</div>
3644
3645 <div><strong>Заказы:</strong> ${this.formatMetric(product.orders, product.ordersChange)}</div>
3646
3647 <div><strong>CR:</strong> ${this.formatMetric(product.cr, product.crChange, true)}</div>
3648
3649 </div>
3650
3651 </div>
3652
3653
3654 <!-- БЛОК 4: Остаток / На дней / Время доставки / Цена -->
3655 <div style="background: #f8f9fa; padding: 10px; border-radius: 6px; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: #2c3e50;">
3656
3657 📦 Логистика и цена
3658
3659 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px;">
3660
3661 <div><strong>Остаток:</strong> ${product.stock || '—'} шт</div>
3662
3663 <div><strong>На дней:</strong> ${product.daysOfStock || '—'}</div>
3664
3665 <div><strong>Доставка:</strong> ${product.deliveryTime || '—'}</div>
3666
3667 <div><strong>Цена:</strong> ${this.formatMetric(product.avgPrice, product.avgPriceChange)} ₽</div>
3668
3669 </div>
3670
3671 </div>
3672
3673
3674 <div style="margin-bottom: 16px;">
3675
3676 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">🔍 Выявленные проблемы</h3>
3677
3678 ${product.analysis.problems.length > 0 ? product.analysis.problems.map(p => `
3679
3680 <div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
3681
3682 <strong>${p.type}:</strong> ${p.description}
3683
3684 </div>
3685
3686 `).join('') : '<div style="color: #27ae60; font-size: 12px;">✅ Проблем не выявлено</div>'}
3687
3688 </div>
3689
3690
3691 <div>
3692
3693 <h3 style="font-size: 14px; color: #2c3e50; margin-bottom: 8px;">💡 Рекомендации</h3>
3694
3695 ${product.analysis.recommendations.map(r => `
3696
3697 <div style="background: #d4edda; padding: 10px; border-radius: 6px; margin-bottom: 8px; font-size: 12px;">
3698
3699 ${r}
3700
3701 </div>
3702
3703 `).join('')}
3704
3705 </div>
3706
3707
3708 <button id="filter-by-article" style="
3709
3710 width: 100%;
3711
3712 margin-top: 16px;
3713
3714 padding: 12px;
3715
3716 background: #667eea;
3717
3718 color: white;
3719
3720 border: none;
3721
3722 border-radius: 8px;
3723
3724 font-size: 14px;
3725
3726 font-weight: 600;
3727
3728 cursor: pointer;
3729
3730 ">
3731
3732 🔍 Показать только этот товар в таблице
3733
3734 </button>
3735
3736 `;
3737
3738
3739 modal.appendChild(modalContent);
3740
3741 document.body.appendChild(modal);
3742
3743
3744 // Закрытие модального окна
3745
3746 modal.addEventListener('click', (e) => {
3747
3748 if (e.target === modal) {
3749
3750 modal.remove();
3751
3752 }
3753
3754 });
3755
3756
3757 modalContent.querySelector('#close-modal').addEventListener('click', () => {
3758
3759 modal.remove();
3760
3761 });
3762
3763
3764 // Фильтрация по артикулу
3765
3766 modalContent.querySelector('#filter-by-article').addEventListener('click', () => {
3767
3768 this.filterByArticle(product.article);
3769
3770 modal.remove();
3771
3772 });
3773
3774 }
3775
3776
3777 filterByArticle(article) {
3778
3779 console.log(`🔍 Фильтруем по артикулу: ${article}`);
3780
3781
3782
3783 // Находим поле фильтра по артикулу на странице
3784
3785 const articleInput = document.querySelector('input[placeholder*="артикул"], input[name*="article"]');
3786
3787
3788
3789 if (articleInput) {
3790
3791 articleInput.value = article;
3792
3793 articleInput.dispatchEvent(new Event('input', { bubbles: true }));
3794
3795 articleInput.dispatchEvent(new Event('change', { bubbles: true }));
3796
3797
3798
3799 // Ищем кнопку "Применить" - ищем все кнопки и проверяем текст
3800
3801 const buttons = document.querySelectorAll('button[type="submit"]');
3802
3803 let applyButton = null;
3804
3805 for (const btn of buttons) {
3806
3807 if (btn.textContent.includes('Применить')) {
3808
3809 applyButton = btn;
3810
3811 break;
3812
3813 }
3814
3815 }
3816
3817
3818
3819 if (applyButton) {
3820
3821 setTimeout(() => applyButton.click(), 300);
3822
3823 }
3824
3825 } else {
3826
3827 console.warn('Не найдено поле для ввода артикула');
3828
3829 }
3830
3831 }
3832
3833 }
3834
3835
3836 // Инициализация
3837
3838 async function init() {
3839
3840 console.log('🎯 Инициализация AI Аналитика Продаж...');
3841
3842
3843 // Проверяем, что мы на странице аналитики
3844
3845 if (!window.location.href.includes('seller.ozon.ru/app/analytics')) {
3846
3847 console.log('⚠️ Не на странице аналитики, ожидаем...');
3848
3849 return;
3850
3851 }
3852
3853
3854 // Ждем загрузки таблицы
3855
3856 const waitForTable = setInterval(() => {
3857
3858 const table = document.querySelector('table.ct590-a');
3859
3860 if (table) {
3861
3862 clearInterval(waitForTable);
3863
3864 console.log('✅ Таблица найдена, создаем UI');
3865
3866 const ui = new AnalyticsUI();
3867
3868 ui.createUI();
3869
3870 }
3871
3872 }, 1000);
3873
3874 }
3875
3876
3877 // Запуск при загрузке страницы
3878
3879 if (document.readyState === 'loading') {
3880
3881 document.addEventListener('DOMContentLoaded', init);
3882
3883 } else {
3884
3885 init();
3886
3887 }
3888
3889
3890 // Отслеживание изменений URL (для SPA)
3891
3892 let lastUrl = location.href;
3893
3894 new MutationObserver(() => {
3895
3896 const url = location.href;
3897
3898 if (url !== lastUrl) {
3899
3900 lastUrl = url;
3901
3902 init();
3903
3904 }
3905
3906 }).observe(document, { subtree: true, childList: true });
3907
3908
3909})();