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