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