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