work-license-server 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/landing.js CHANGED
@@ -15,11 +15,15 @@
15
15
  let currentUser = null;
16
16
  let currentStage = 'product';
17
17
  let selectedCode = null;
18
+ let pendingAuthAction = null;
18
19
 
19
20
  async function api(path, body, method) {
21
+ const headers = {};
22
+ if (body) headers['Content-Type'] = 'application/json';
23
+ if (currentUser?.sessionKey) headers['Authorization'] = 'Bearer ' + currentUser.sessionKey;
20
24
  const res = await fetch(path, {
21
25
  method: method || (body ? 'POST' : 'GET'),
22
- headers: body ? { 'Content-Type': 'application/json' } : undefined,
26
+ headers: Object.keys(headers).length ? headers : undefined,
23
27
  body: body ? JSON.stringify(body) : undefined,
24
28
  });
25
29
  let data = {};
@@ -29,6 +33,10 @@
29
33
  return data;
30
34
  }
31
35
 
36
+ function isUserReadyForOrders() {
37
+ return Boolean(currentUser && currentUser.ofertaAccepted);
38
+ }
39
+
32
40
  /* ================= Тема ================= */
33
41
  function applyTheme(theme) {
34
42
  document.documentElement.setAttribute('data-theme', theme);
@@ -53,21 +61,20 @@
53
61
  ind.style.width = active.offsetWidth + 'px';
54
62
  }
55
63
  function switchStage(id, { scroll = true } = {}) {
56
- if (id === 'addons' && !currentUser) { openAuth(); return; }
64
+ if (id === 'addons' && !isUserReadyForOrders()) {
65
+ requireAuthAndOferta(() => switchStage('addons'));
66
+ return;
67
+ }
57
68
  if (!$('#stage-' + id)) id = 'product';
58
69
  currentStage = id;
70
+ document.body.setAttribute('data-stage', id);
59
71
  $$('.tab').forEach(t => t.classList.toggle('active', t.dataset.stage === id));
60
72
  $$('.stage').forEach(s => s.classList.toggle('active', s.id === 'stage-' + id));
61
- const admBtn = $('#btnAdminTop');
62
- if (admBtn) {
63
- admBtn.classList.toggle('btn-primary', id === 'admin');
64
- admBtn.classList.toggle('btn-ghost', id !== 'admin');
65
- }
66
73
  moveIndicator();
67
74
  const active = $('.tab.active');
68
75
  if (active) active.scrollIntoView({ block: 'nearest', inline: 'center', behavior: reduceMotion ? 'auto' : 'smooth' });
69
76
  if (history.replaceState) {
70
- const url = id === 'product' ? '/' : (id === 'admin' && location.pathname === '/admin' ? '/admin' : '/#' + id);
77
+ const url = id === 'product' ? '/' : '/#' + id;
71
78
  history.replaceState(null, '', url);
72
79
  }
73
80
  if (scroll) window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
@@ -75,7 +82,6 @@
75
82
  observeReveals();
76
83
  if (id === 'agents') playConsultantMock();
77
84
  if (id === 'franchise') requestAnimationFrame(() => MAP.ready && applyView());
78
- if (id === 'admin') refreshRegistry();
79
85
  }
80
86
  window.switchStage = switchStage;
81
87
 
@@ -123,46 +129,199 @@
123
129
  card.style.setProperty('--my', (e.clientY - r.top) + 'px');
124
130
  }, { passive: true });
125
131
 
126
- /* ================= Авторизация ================= */
127
- function openAuth() { $('#authModal').classList.add('open'); setTimeout(() => $('#authEmail').focus(), 50); }
128
- function closeAuth() { $('#authModal').classList.remove('open'); }
132
+ /* ================= Авторизация и подписание публичной оферты (ПЭП) ================= */
133
+ function openAuth(hintMsg = '') {
134
+ if ($('#authErrorMsg')) $('#authErrorMsg').textContent = hintMsg || '';
135
+ if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = currentUser ? '' : 'none';
136
+ $('#authModal').classList.add('open');
137
+ setTimeout(() => $('#authEmail')?.focus(), 50);
138
+ }
139
+ function closeAuth() {
140
+ $('#authModal').classList.remove('open');
141
+ if ($('#authErrorMsg')) $('#authErrorMsg').textContent = '';
142
+ }
143
+ function openOfertaModal() {
144
+ $('#ofertaModal')?.classList.add('open');
145
+ }
146
+ function closeOfertaModal() {
147
+ $('#ofertaModal')?.classList.remove('open');
148
+ }
149
+ function requireAuthAndOferta(actionFn, prefill = {}) {
150
+ if (isUserReadyForOrders()) {
151
+ if (typeof actionFn === 'function') actionFn();
152
+ return true;
153
+ }
154
+ pendingAuthAction = typeof actionFn === 'function' ? actionFn : null;
155
+ if (prefill.companyName && $('#authCompany')) $('#authCompany').value = prefill.companyName;
156
+ if (prefill.inn && $('#authInn')) $('#authInn').value = prefill.inn;
157
+ if (prefill.email && $('#authEmail')) $('#authEmail').value = prefill.email;
158
+ openAuth('Для оформления заказа, оплаты или резервирования франшизы зарегистрируйтесь и подпишите договор-оферту (ПЭП).');
159
+ return false;
160
+ }
161
+
129
162
  async function performLogin() {
163
+ const errBox = $('#authErrorMsg');
164
+ if (errBox) errBox.textContent = '';
165
+ const email = $('#authEmail')?.value.trim() || '';
166
+ const companyName = $('#authCompany')?.value.trim() || '';
167
+ const inn = $('#authInn')?.value.trim() || '';
168
+ const signer = $('#authSigner')?.value.trim() || '';
169
+ const licenseId = $('#authLicId')?.value.trim() || '';
170
+ const ofertaChecked = Boolean($('#authOfertaCheck')?.checked);
171
+
172
+ if (!email || !email.includes('@')) {
173
+ if (errBox) errBox.textContent = 'Укажите корректный корпоративный email.';
174
+ return;
175
+ }
176
+ if (!companyName || !inn) {
177
+ if (errBox) errBox.textContent = 'Укажите наименование организации и ИНН.';
178
+ return;
179
+ }
180
+ if (!ofertaChecked) {
181
+ if (errBox) errBox.textContent = 'Для продолжения необходимо ознакомиться с условиями Публичной оферты и поставить отметку о подписании ПЭП.';
182
+ return;
183
+ }
184
+
130
185
  try {
131
- const data = await api('/api/auth/login', {
132
- email: $('#authEmail').value.trim(),
133
- companyName: $('#authCompany').value.trim(),
134
- licenseId: $('#authLicId').value.trim(),
186
+ const data = await api('/api/auth/register', {
187
+ email,
188
+ companyName,
189
+ inn,
190
+ name: signer || companyName,
191
+ licenseId,
192
+ ofertaAccepted: true,
135
193
  });
136
194
  currentUser = data;
137
195
  try { localStorage.setItem('odant_work_session', data.sessionKey); } catch { /* приватный режим */ }
138
196
  onAuthenticated();
139
197
  closeAuth();
140
- switchStage('addons');
198
+ const nextAction = pendingAuthAction;
199
+ pendingAuthAction = null;
200
+ if (typeof nextAction === 'function') {
201
+ nextAction();
202
+ }
203
+ else {
204
+ switchStage('addons');
205
+ }
141
206
  }
142
207
  catch (err) {
143
- alert(err.message);
208
+ if (errBox) errBox.textContent = err.message;
144
209
  }
145
210
  }
211
+
212
+ function performLogout() {
213
+ currentUser = null;
214
+ pendingAuthAction = null;
215
+ try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
216
+ $('#tileAddons').style.display = 'none';
217
+ $('#btnAuthText').textContent = 'Войти / Регистрация';
218
+ if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = 'none';
219
+ updateOfertaBanners();
220
+ updateClientSupportCard();
221
+ closeAuth();
222
+ if (currentStage === 'addons') switchStage('product');
223
+ moveIndicator();
224
+ }
225
+
146
226
  async function restoreSession() {
147
227
  let key = '';
148
228
  try { key = localStorage.getItem('odant_work_session') || ''; } catch { /* приватный режим */ }
149
- if (!key) return;
229
+ if (!key) {
230
+ updateOfertaBanners();
231
+ updateClientSupportCard();
232
+ return;
233
+ }
150
234
  try {
151
235
  const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
152
- if (data.authenticated) {
236
+ if (data.authenticated && data.user) {
153
237
  currentUser = { ...data.user, sessionKey: key };
154
238
  onAuthenticated();
155
- if (location.hash === '#addons') switchStage('addons', { scroll: false });
239
+ if (location.hash === '#addons' && isUserReadyForOrders()) switchStage('addons', { scroll: false });
156
240
  }
157
241
  }
158
242
  catch { /* сессия устарела */ }
243
+ updateOfertaBanners();
244
+ updateClientSupportCard();
159
245
  }
246
+
247
+ function updateOfertaBanners() {
248
+ const ready = isUserReadyForOrders();
249
+ $$('[data-oferta-banner]').forEach(box => {
250
+ box.classList.toggle('signed', ready);
251
+ if (ready) {
252
+ box.innerHTML = `
253
+ <div>
254
+ <span class="tag ok">✓ Договор-оферта подписан ПЭП</span>
255
+ <div style="margin-top:6px;font-size:13.5px">
256
+ Организация: <b>${esc(currentUser.companyName)}</b> (ИНН ${esc(currentUser.inn || '—')}) · Подписант: <b>${esc(currentUser.ofertaSignerName || currentUser.name || '—')}</b> · Штамп ПЭП: <code>${esc(currentUser.ofertaSignatureId || 'PEP')}</code>
257
+ </div>
258
+ </div>
259
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
260
+ <button class="btn btn-ghost btn-sm" data-action="open-oferta">Текст оферты</button>
261
+ <button class="btn btn-ghost btn-sm" data-action="auth">Профиль</button>
262
+ </div>`;
263
+ }
264
+ else {
265
+ box.innerHTML = `
266
+ <div>
267
+ <span class="tag">Требуется регистрация и акцепт оферты</span>
268
+ <div style="margin-top:6px;font-size:13.5px;color:var(--text-2)">
269
+ Любые действия по заказу комплекса, докупке лицензий, оплате через ЮKassa и резервированию региональной франшизы выполняются после регистрации и подписания Публичного договора-оферты (ПЭП).
270
+ </div>
271
+ </div>
272
+ <div style="display:flex;gap:8px;flex-wrap:wrap">
273
+ <button class="btn btn-ghost btn-sm" data-action="open-oferta">Читать оферту</button>
274
+ <button class="btn btn-primary btn-sm" data-action="auth">Войти и подписать оферту</button>
275
+ </div>`;
276
+ }
277
+ });
278
+ }
279
+
280
+ function updateClientSupportCard() {
281
+ const box = $('#clientLicenseBox');
282
+ if (!box) return;
283
+ if (!currentUser || !currentUser.license) {
284
+ box.textContent = 'Войдите в личный кабинет и подпишите договор-оферту, чтобы получить код привязки вашего комплекса к ботам MAX, Telegram и VK.';
285
+ return;
286
+ }
287
+ const lic = currentUser.license;
288
+ const bindCode = lic.support?.bindCode || lic.licenseId;
289
+ box.textContent = [
290
+ `Организация: ${currentUser.companyName} (ИНН ${currentUser.inn || '—'})`,
291
+ `Оферта ПЭП: ${currentUser.ofertaAccepted ? ('подписана · ' + (currentUser.ofertaSignatureId || '')) : 'ожидает подписания'}`,
292
+ `Лицензия ПАК: ${lic.licenseId}`,
293
+ `Код привязки мессенджер-бота: ${bindCode}`,
294
+ `Команда для чата MAX / Telegram / VK: /bind ${bindCode}`,
295
+ `Рабочие места: ${lic.totalSeats} (${lic.baseSeats} вечных + ${lic.addonSeats} годовых)`,
296
+ `Уровень поддержки SLA: ${lic.support?.tierName || 'Enterprise'} (${lic.support?.reactionLabel || 'до 15 мин'})`,
297
+ `Остаток обращений: ${lic.support?.ticketsRemaining ?? 0} из ${lic.support?.ticketsQuota ?? 0}`,
298
+ ].join('\n');
299
+ }
300
+
160
301
  function onAuthenticated() {
161
302
  if (!currentUser) return;
162
- $('#tileAddons').style.display = '';
303
+ $('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
163
304
  $('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
164
- $('#inpLicId').value = currentUser.licenseId || '';
165
- $('#userBadgeBox').innerHTML = `${esc(currentUser.companyName)} · лицензия <b>${esc(currentUser.licenseId)}</b>`;
305
+ if ($('#inpLicId')) $('#inpLicId').value = currentUser.licenseId || '';
306
+ if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = currentUser.hubToken || '';
307
+ if ($('#authEmail')) $('#authEmail').value = currentUser.email || '';
308
+ if ($('#authCompany')) $('#authCompany').value = currentUser.companyName || '';
309
+ if ($('#authInn') && currentUser.inn) $('#authInn').value = currentUser.inn;
310
+ if ($('#authSigner') && (currentUser.ofertaSignerName || currentUser.name)) $('#authSigner').value = currentUser.ofertaSignerName || currentUser.name;
311
+ if ($('#authLicId')) $('#authLicId').value = currentUser.licenseId || '';
312
+ if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = Boolean(currentUser.ofertaAccepted);
313
+ if ($('#inpPakOrg') && currentUser.companyName) $('#inpPakOrg').value = currentUser.companyName;
314
+ if ($('#inpPakInn') && currentUser.inn) $('#inpPakInn').value = currentUser.inn;
315
+ if ($('#inpPakEmail') && currentUser.email) $('#inpPakEmail').value = currentUser.email;
316
+ if ($('#inpPartnerCompany') && currentUser.companyName) $('#inpPartnerCompany').value = currentUser.companyName;
317
+ if ($('#inpPartnerInn') && currentUser.inn) $('#inpPartnerInn').value = currentUser.inn;
318
+ if ($('#inpPartnerEmail') && currentUser.email) $('#inpPartnerEmail').value = currentUser.email;
319
+ if ($('#botText') && currentUser.license?.support?.bindCode && $('#botText').value === '/start') {
320
+ $('#botText').value = '/bind ' + currentUser.license.support.bindCode;
321
+ }
322
+ $('#userBadgeBox').innerHTML = `${esc(currentUser.companyName)} (ИНН ${esc(currentUser.inn || '—')}) · лицензия <b>${esc(currentUser.licenseId)}</b> · ПЭП <code>${esc(currentUser.ofertaSignatureId || '—')}</code>`;
323
+ updateOfertaBanners();
324
+ updateClientSupportCard();
166
325
  moveIndicator();
167
326
  recalcAddons();
168
327
  }
@@ -203,7 +362,7 @@
203
362
  ['Мест × лет', `${seats} × ${years}`],
204
363
  ['Без скидки', rub(gross)],
205
364
  ['Скидка', `−${disc.discountPercent}%`],
206
- ['Всего мест после докупки', `${(D.product?.baseSeats || 5) + seats}`],
365
+ ['Всего мест после докупки', `${(D.product?.baseSeats || 10) + seats}`],
207
366
  ]);
208
367
  markLadder('ladderAddons', seats);
209
368
  }
@@ -214,54 +373,97 @@
214
373
  recalcAddons();
215
374
  }
216
375
  function recalcBaseOrder() {
376
+ const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
377
+ const isLicenseOnly = variant === 'license_only';
378
+ const basePrice = isLicenseOnly ? (D.product.baseLicensePriceRub || 4_000_000) : D.product.basePriceRub;
217
379
  const extra = Math.max(0, Number($('#inpPakExtraSeats').value) || 0);
218
380
  const tier = tierById(segValue('segPakTier'));
219
381
  const disc = volumeDiscount(extra);
220
382
  const gross = extra * tier.pricePerUserYearRub;
221
383
  const net = gross - Math.round(gross * disc.discountPercent / 100);
222
384
  $('#lblPakSeats').textContent = extra;
223
- $('#lblPakTotal').textContent = rub(D.product.basePriceRub + net);
385
+ $('#lblPakTotal').textContent = rub(basePrice + net);
224
386
  const rows = [
225
- ['Комплекс ODANT-WORK', rub(D.product.basePriceRub)],
387
+ [isLicenseOnly ? 'Базовая лицензия (без железа)' : 'Комплекс ODANT-WORK (с машиной)', rub(basePrice)],
226
388
  ['Вечные рабочие места', String(D.product.baseSeats)],
227
- ['Поддержка в комплекте', '1 год · Business'],
389
+ ['Поддержка в комплекте', '1 год · Enterprise (далее — по желанию)'],
228
390
  ];
229
391
  if (extra > 0) rows.push([`Доп. места · ${esc(tier.name)}${disc.discountPercent ? ' · −' + disc.discountPercent + '%' : ''}`, `${extra} · ${rub(net)}`]);
230
392
  $('#lblPakBreakdown').innerHTML = lines(rows);
231
393
  }
232
- async function submitBasePakOrder() {
394
+ async function submitBasePakOrder(paymentMethod = 'yookassa') {
395
+ if (!isUserReadyForOrders()) {
396
+ requireAuthAndOferta(() => submitBasePakOrder(paymentMethod), {
397
+ companyName: $('#inpPakOrg')?.value.trim(),
398
+ inn: $('#inpPakInn')?.value.trim(),
399
+ email: $('#inpPakEmail')?.value.trim(),
400
+ });
401
+ return;
402
+ }
403
+ const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
404
+ const isLicenseOnly = variant === 'license_only';
233
405
  const st = $('#pakOrderStatus');
234
- st.textContent = 'Формируем счёт ЮKassa…';
406
+ st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
235
407
  try {
236
408
  const data = await api('/api/orders/checkout', {
237
- includeBasePak: true,
409
+ sessionKey: currentUser?.sessionKey || '',
410
+ includeBasePak: !isLicenseOnly,
411
+ includeBaseLicenseOnly: isLicenseOnly,
412
+ paymentMethod,
238
413
  tier: segValue('segPakTier'),
239
414
  addonSeats: Number($('#inpPakExtraSeats').value) || 0,
240
415
  years: 1,
241
416
  customer: { name: $('#inpPakOrg').value.trim(), inn: $('#inpPakInn').value.trim(), email: $('#inpPakEmail').value.trim() },
242
417
  });
243
- st.innerHTML = `Счёт <b>${esc(data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
244
- refreshRegistry();
418
+ if (paymentMethod === 'invoice') {
419
+ st.innerHTML = `Счёт-договор <b>${esc(data.invoiceNumber || data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.invoiceUrl || data.confirmationUrl)}" target="_blank" rel="noopener">Открыть печатную форму счёта →</a>`;
420
+ }
421
+ else {
422
+ st.innerHTML = `Счёт <b>${esc(data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
423
+ }
424
+ refreshCatalogRegions();
245
425
  }
246
426
  catch (err) { st.textContent = err.message; }
247
427
  }
248
- async function submitAddonOrder() {
428
+ async function submitAddonOrder(paymentMethod = 'yookassa') {
429
+ if (!isUserReadyForOrders()) {
430
+ requireAuthAndOferta(() => submitAddonOrder(paymentMethod));
431
+ return;
432
+ }
249
433
  const st = $('#orderStatus');
250
- st.textContent = 'Формируем счёт ЮKassa…';
434
+ st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
251
435
  try {
252
436
  const data = await api('/api/orders/checkout', {
437
+ sessionKey: currentUser?.sessionKey || '',
253
438
  includeBasePak: false,
439
+ paymentMethod,
254
440
  tier: segValue('segTier'),
255
441
  addonSeats: Number($('#inpSeats').value) || 1,
256
442
  years: Number($('#inpYears').value) || 1,
257
443
  licenseId: $('#inpLicId').value.trim(),
258
444
  customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
259
445
  });
260
- st.innerHTML = `Заказ <b>${esc(data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
261
- refreshRegistry();
446
+ if (paymentMethod === 'invoice') {
447
+ st.innerHTML = `Счёт-договор <b>${esc(data.invoiceNumber || data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.invoiceUrl || data.confirmationUrl)}" target="_blank" rel="noopener">Открыть счёт на оплату →</a>`;
448
+ }
449
+ else {
450
+ st.innerHTML = `Заказ <b>${esc(data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
451
+ }
452
+ refreshCatalogRegions();
262
453
  }
263
454
  catch (err) { st.textContent = err.message; }
264
455
  }
456
+ async function generateCabinetToken() {
457
+ if (!currentUser?.sessionKey) return;
458
+ try {
459
+ const res = await api('/api/cabinet/token', { sessionKey: currentUser.sessionKey });
460
+ if (res?.token) {
461
+ currentUser.hubToken = res.token;
462
+ if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = res.token;
463
+ }
464
+ }
465
+ catch (err) { alert(err.message); }
466
+ }
265
467
 
266
468
  function recalcPartner() {
267
469
  const m = Number($('#rngPartnerSales').value) || 0;
@@ -640,10 +842,19 @@
640
842
  code = code || selectedCode;
641
843
  if (!code) return;
642
844
  selectRegion(code);
845
+ if (!isUserReadyForOrders()) {
846
+ requireAuthAndOferta(() => reserveRegionFranchise(code), {
847
+ companyName: $('#inpPartnerCompany')?.value.trim(),
848
+ inn: $('#inpPartnerInn')?.value.trim(),
849
+ email: $('#inpPartnerEmail')?.value.trim(),
850
+ });
851
+ return;
852
+ }
643
853
  const msg = $('#partnerActionMsg');
644
854
  msg.textContent = 'Оформляем резерв и счёт на залог…';
645
855
  try {
646
856
  const data = await api('/api/partners/reserve', {
857
+ sessionKey: currentUser?.sessionKey || '',
647
858
  regionCode: code,
648
859
  companyName: $('#inpPartnerCompany').value.trim(),
649
860
  inn: $('#inpPartnerInn').value.trim(),
@@ -654,245 +865,47 @@
654
865
  renderRegionsTable();
655
866
  selectRegion(code);
656
867
  msg.innerHTML = `Квота в субъекте «${esc(data.reservation.regionName)}» закреплена за вами (резерв <b>${esc(data.reservation.reservationId)}</b>). <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Оплатить залог ${rub(data.amountRub)} →</a>`;
657
- refreshRegistry();
868
+ refreshCatalogRegions();
658
869
  }
659
870
  catch (err) { msg.textContent = err.message; }
660
871
  }
661
872
  window.reserveRegionFranchise = reserveRegionFranchise;
662
873
 
663
- /* ================= Поддержка ================= */
874
+ /* ================= Поддержка и синхронизация квот ================= */
664
875
  async function sendBot() {
665
876
  const box = $('#botReplyBox');
666
877
  try {
667
878
  const data = await api('/api/bots/' + $('#botChannel').value + '/webhook', {
668
- chat_id: $('#botChatId').value.trim(), text: $('#botText').value, user_name: 'Клиент ODANT-WORK',
879
+ chat_id: $('#botChatId').value.trim(),
880
+ text: $('#botText').value,
881
+ user_name: currentUser?.companyName || 'Клиент ODANT-WORK',
669
882
  });
670
883
  box.textContent = data.reply || data.result?.reply || JSON.stringify(data, null, 2);
671
884
  }
672
885
  catch (err) { box.textContent = err.message; }
673
- refreshRegistry();
674
- }
675
- async function createDemoPak() {
676
- try {
677
- const lic = await api('/api/license/issue', { customer: { name: 'ООО «Маслоторг»', inn: '7701234567', email: 'admin@maslotorg.ru' } });
678
- if (lic.licenseId) $('#botText').value = '/bind ' + lic.licenseId;
679
- }
680
- catch { /* показываем реестр как есть */ }
681
- refreshRegistry();
682
- }
683
- let lastAdminData = null;
684
-
685
- async function refreshRegistry() {
686
- let data;
687
- try { data = await api('/api/admin/overview'); }
688
- catch { return; }
689
- lastAdminData = data;
690
- if (Array.isArray(data.regions) && data.regions.length) {
691
- data.regions.forEach(mergeRegion);
692
- paintMap();
693
- renderRegionsTable();
694
- if (selectedCode) selectRegion(selectedCode);
695
- }
696
- const out = [];
697
- out.push(`Лицензии (${data.licenses?.length || 0})`);
698
- for (const l of data.licenses || [])
699
- out.push(` ${l.licenseId} · ${l.customer?.name || '—'} · мест ${l.totalSeats} (${l.baseSeats ?? 5} вечных + ${l.addonSeats}) · ${l.support?.tierName || ''}`);
700
- out.push('', `Резервы франшиз (${data.reservations?.length || 0})`);
701
- for (const r of data.reservations || [])
702
- out.push(` ${r.reservationId} · ${r.regionName} · ${r.companyName} · залог ${r.depositPaid ? 'оплачен' : 'ожидает оплаты'}`);
703
- out.push('', `Обращения (${data.tickets?.length || 0})`);
704
- for (const t of data.tickets || [])
705
- out.push(` ${t.ticketId} [${String(t.channel).toUpperCase()}] ${t.tierName}: ${t.messages?.[0]?.text || ''}`);
706
- if ($('#registryBox')) $('#registryBox').textContent = out.join('\n');
707
- renderAdminPanel(data);
708
- }
709
-
710
- function fmtDate(ms) {
711
- if (!ms) return '—';
712
- try { return new Date(ms).toLocaleDateString('ru-RU'); } catch { return '—'; }
713
- }
714
-
715
- function renderAdminPanel(data = {}) {
716
- const licenses = data.licenses || [];
717
- const orders = data.orders || [];
718
- const partners = data.partners || [];
719
- const reservations = data.reservations || [];
720
- const tickets = data.tickets || [];
721
-
722
- const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
723
- const paidOrders = orders.filter(o => o.status === 'paid');
724
- const pendingOrders = orders.filter(o => o.status === 'pending');
725
- const paidRub = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
726
- const pendingRub = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
727
- const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
728
-
729
- if ($('#admStatLic')) {
730
- $('#admStatLic').textContent = String(licenses.length);
731
- $('#admStatLicSub').textContent = `лицензий ПАК · ${totalSeats} активных мест`;
732
- $('#admStatPaid').textContent = rub(paidRub);
733
- $('#admStatPaidSub').textContent = `оплачено заказов: ${paidOrders.length}`;
734
- $('#admStatPending').textContent = rub(pendingRub);
735
- $('#admStatPendingSub').textContent = `ожидают оплаты: ${pendingOrders.length}`;
736
- $('#admStatPartners').textContent = `${partners.length} / ${reservations.length}`;
737
- $('#admStatTickets').textContent = String(openTickets.length);
738
- $('#admStatTicketsSub').textContent = `открытых из ${tickets.length} обращений`;
739
- }
740
-
741
- if ($('#admLicensesBody')) {
742
- $('#admLicensesBody').innerHTML = licenses.map(l => `<tr>
743
- <td><b>${esc(l.licenseId)}</b><div class="small muted">Код бота: <code>${esc(l.bindingCode || l.licenseId)}</code></div></td>
744
- <td><b>${esc(l.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(l.customer?.inn || '—')} · ${esc(l.customer?.email || '—')}</div></td>
745
- <td><code>${esc(l.boundHwid || 'не привязан')}</code><div class="small muted">${esc((l.boundNodeFingerprint || '').slice(0, 18) || 'ожидает синхронизации')}</div></td>
746
- <td class="num"><b>${l.totalSeats}</b> <span class="muted">(${l.baseSeats ?? 5} вечн. + ${l.addonSeats || 0} доп.)</span></td>
747
- <td><span class="tag ok">${esc(l.support?.tierName || 'Business')}</span><div class="small muted">до ${fmtDate(l.support?.expiresAt)}</div></td>
748
- <td><button class="btn btn-ghost btn-sm" data-cert="${esc(l.licenseId)}">Сертификат JSON</button></td>
749
- </tr>`).join('') || '<tr><td colspan="6" class="muted">Лицензий пока нет — нажмите «+ Демо-лицензия ПАК»</td></tr>';
750
- }
751
-
752
- if ($('#admOrdersBody')) {
753
- $('#admOrdersBody').innerHTML = orders.map(o => {
754
- const stTag = o.status === 'paid' ? 'ok' : (o.status === 'canceled' ? 'bad' : '');
755
- const stLabel = o.status === 'paid' ? 'оплачен' : (o.status === 'canceled' ? 'отменён' : 'ожидает оплаты');
756
- return `<tr>
757
- <td><b>${esc(o.orderId)}</b><div class="small muted">${esc(o.paymentId || '—')}</div></td>
758
- <td><b>${esc(o.tierName || o.kind || 'Заказ')}</b><div class="small muted">${o.includeBasePak ? 'Комплекс ПАК' : (o.kind === 'franchise_reservation' ? 'Залог франшизы' : `Доп. места: ${o.addonSeats || 0} × ${o.years || 1} г.`)}</div></td>
759
- <td><b>${esc(o.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(o.customer?.inn || '—')}</div></td>
760
- <td class="num"><b>${rub(o.amountRub)}</b>${o.volumeDiscountPercent ? `<div class="small" style="color:var(--ok)">скидка −${o.volumeDiscountPercent}%</div>` : ''}</td>
761
- <td><span class="tag ${stTag}">${stLabel}</span></td>
762
- <td><div style="display:flex;gap:6px;flex-wrap:wrap">
763
- ${o.status === 'pending' ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(o.orderId)}">Провести оплату</button>` : ''}
764
- <a class="btn btn-ghost btn-sm" href="/pay/sandbox/${encodeURIComponent(o.orderId)}" target="_blank" rel="noopener">Песочница</a>
765
- </div></td>
766
- </tr>`;
767
- }).join('') || '<tr><td colspan="6" class="muted">Заказов пока нет</td></tr>';
768
- }
769
-
770
- if ($('#admPartnersBody')) {
771
- const resRows = reservations.map(r => `<tr>
772
- <td><b>${esc(r.reservationId)}</b><div class="small muted">Резерв франшизы</div></td>
773
- <td><b>${esc(r.regionName)}</b> <span class="muted">(${esc(r.regionCode)})</span></td>
774
- <td><b>${esc(r.companyName)}</b><div class="small muted">ИНН ${esc(r.inn || '—')} · ${esc(r.email || '—')}</div></td>
775
- <td>Паушальный взнос <b>${rub(r.franchiseFeeRub)}</b><div class="small muted">Залог ${rub(r.nonRefundableDepositRub || r.depositAmountRub || D.franchise.reservationDepositRub)} · полная оплата за 1 год</div></td>
776
- <td><span class="tag ${r.depositPaid ? 'ok' : ''}">${r.depositPaid ? 'залог оплачен' : 'ждёт оплаты залога'}</span></td>
777
- <td><div style="display:flex;gap:6px;flex-wrap:wrap">
778
- ${!r.depositPaid && r.orderId ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(r.orderId)}">Оплатить залог</button>` : ''}
779
- ${!r.convertedToPartnerId ? `<button class="btn btn-primary btn-sm" data-activate-res="${esc(r.reservationId)}">Активировать партнёра</button>` : `<span class="small muted">Партнёр ${esc(r.convertedToPartnerId)}</span>`}
780
- </div></td>
781
- </tr>`);
782
- const partRows = partners.map(p => `<tr>
783
- <td><b>${esc(p.partnerId)}</b><div class="small muted">Действующий партнёр</div></td>
784
- <td><b>${esc(p.regionName)}</b> <span class="muted">(${esc(p.regionCode)})</span></td>
785
- <td><b>${esc(p.companyName)}</b><div class="small muted">ИНН ${esc(p.inn || '—')}</div></td>
786
- <td>Скидка <b>${p.discountPercent}%</b> · ${esc(p.statusLevel)}<div class="small muted">Продажи: ${rub(p.annualSalesRub || 0)}</div></td>
787
- <td><span class="tag ok">сертификатов: ${(p.certifiedSpecialists || []).length}</span></td>
788
- <td><button class="btn btn-ghost btn-sm" data-certify-partner="${esc(p.partnerId)}">+ Аттестовать инженера</button></td>
789
- </tr>`);
790
- $('#admPartnersBody').innerHTML = [...resRows, ...partRows].join('') || '<tr><td colspan="6" class="muted">Резервов и партнёров пока нет — выберите субъект во вкладке «Франшиза»</td></tr>';
791
- }
792
-
793
- if ($('#admTicketsBody')) {
794
- $('#admTicketsBody').innerHTML = tickets.map(t => {
795
- const msgs = (t.messages || []).map(m => `<div class="small"><b>${esc(m.role === 'engineer' ? (m.author || 'Инженер') : (t.userName || 'Клиент'))}:</b> ${esc(m.text)}</div>`).join('');
796
- return `<tr>
797
- <td><b>${esc(t.ticketId)}</b><div><span class="tag">${esc(String(t.channel || '').toUpperCase())}</span></div></td>
798
- <td><b>${esc(t.userName || t.chatId)}</b><div class="small muted">Лицензия: ${esc(t.licenseId || 'не привязана')}</div></td>
799
- <td><span class="tag ok">${esc(t.tierName || 'Start')}</span><div class="small muted">реакция ≤ ${t.slaReactionMinutes || 480} мин</div></td>
800
- <td style="white-space:normal;max-width:360px">${msgs}</td>
801
- <td>
802
- <div style="display:flex;gap:6px">
803
- <input type="text" id="reply-${esc(t.ticketId)}" placeholder="Ответ инженера…" style="min-width:190px;padding:7px 10px;font-size:12.5px">
804
- <button class="btn btn-primary btn-sm" data-reply-ticket="${esc(t.ticketId)}">Ответить</button>
805
- </div>
806
- </td>
807
- </tr>`;
808
- }).join('') || '<tr><td colspan="5" class="muted">Обращений пока нет — отправьте сообщение через «Консоль бота» во вкладке «Поддержка»</td></tr>';
809
- }
810
- }
811
-
812
- async function adminIssueLicense() {
813
- const st = $('#admIssueStatus');
814
- st.textContent = 'Выпускаем сертификат…';
815
- try {
816
- const lic = await api('/api/license/issue', {
817
- hwid: $('#admLicHwid').value.trim(),
818
- customer: {
819
- name: $('#admLicOrg').value.trim(),
820
- inn: $('#admLicInn').value.trim(),
821
- email: $('#admLicEmail').value.trim(),
822
- },
823
- });
824
- st.textContent = `Выпущена лицензия ${lic.licenseId} (${lic.totalSeats} мест, подпись Ed25519)`;
825
- refreshRegistry();
826
- }
827
- catch (err) { st.textContent = err.message; }
828
- }
829
-
830
- async function adminConfirmOrder(orderId) {
831
- const order = (lastAdminData?.orders || []).find(o => o.orderId === orderId);
832
- if (!order) return;
833
- await api('/api/payments/yookassa/webhook', {
834
- type: 'notification',
835
- event: 'payment.succeeded',
836
- object: {
837
- id: order.paymentId,
838
- status: 'succeeded',
839
- amount: { value: Number(order.amountRub || 0).toFixed(2), currency: 'RUB' },
840
- metadata: { orderId: order.orderId, licenseId: order.licenseId || '', reservationId: order.reservationId || '' },
841
- },
842
- });
843
- refreshRegistry();
844
- }
845
-
846
- async function adminActivateReservation(reservationId) {
847
- const r = (lastAdminData?.reservations || []).find(x => x.reservationId === reservationId);
848
- if (!r) return;
849
- try {
850
- await api('/api/partners/register', {
851
- reservationId: r.reservationId,
852
- regionCode: r.regionCode,
853
- companyName: r.companyName,
854
- inn: r.inn,
855
- email: r.email,
856
- franchiseFeePaid: true,
857
- });
858
- refreshRegistry();
886
+ if (currentUser?.sessionKey) {
887
+ try {
888
+ const me = await api('/api/auth/me?sessionKey=' + encodeURIComponent(currentUser.sessionKey));
889
+ if (me.authenticated && me.user) {
890
+ currentUser = { ...me.user, sessionKey: currentUser.sessionKey };
891
+ updateClientSupportCard();
892
+ }
893
+ }
894
+ catch { /* ignore */ }
859
895
  }
860
- catch (err) { alert(err.message); }
861
896
  }
862
897
 
863
- async function adminCertifyPartner(partnerId) {
898
+ async function refreshCatalogRegions() {
864
899
  try {
865
- await api('/api/partners/certify', {
866
- partnerId,
867
- specialistName: 'Ведущий инженер ПАК',
868
- specialistEmail: 'engineer@partner.ru',
869
- trackCode: 'TRK-PAK-HW',
870
- examScore: 96,
871
- });
872
- refreshRegistry();
900
+ const data = await api('/api/catalog');
901
+ if (Array.isArray(data.regions) && data.regions.length) {
902
+ data.regions.forEach(mergeRegion);
903
+ paintMap();
904
+ renderRegionsTable();
905
+ if (selectedCode) selectRegion(selectedCode);
906
+ }
873
907
  }
874
- catch (err) { alert(err.message); }
875
- }
876
-
877
- async function adminReplyTicket(ticketId) {
878
- const inp = document.getElementById('reply-' + ticketId);
879
- const text = inp?.value.trim();
880
- if (!text) return;
881
- await api('/api/tickets/' + encodeURIComponent(ticketId) + '/reply', {
882
- engineerName: 'Дежурный инженер ODANT',
883
- text,
884
- });
885
- inp.value = '';
886
- refreshRegistry();
887
- }
888
-
889
- function openCertModal(licenseId) {
890
- const lic = (lastAdminData?.licenses || []).find(l => l.licenseId === licenseId);
891
- if (!lic) return;
892
- $('#certTitle').textContent = `Сертификат ${lic.licenseId}`;
893
- $('#certSub').textContent = `${lic.customer?.name || ''} · Ed25519 (WORK-LIC-v1)`;
894
- $('#certJsonBox').textContent = JSON.stringify(lic.certificate || lic, null, 2);
895
- $('#certModal').classList.add('open');
908
+ catch { /* ignore */ }
896
909
  }
897
910
 
898
911
  /* ================= ИИ-агент лицензионного центра ================= */
@@ -903,7 +916,10 @@
903
916
  if (open) setTimeout(() => $('#aiInput').focus(), 60);
904
917
  }
905
918
  function md(text) {
906
- return esc(text).replace(/\*\*(.+?)\*\*/g, '<b>$1</b>').replace(/(^|\s)\*(.+?)\*(?=\s|$)/g, '$1<i>$2</i>');
919
+ return esc(text)
920
+ .replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
921
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
922
+ .replace(/(^|\s)\*(.+?)\*(?=\s|$)/g, '$1<i>$2</i>');
907
923
  }
908
924
  async function sendAi(text) {
909
925
  const inp = $('#aiInput');
@@ -924,14 +940,20 @@
924
940
  switchStage('franchise');
925
941
  setTimeout(() => selectRegion(act.regionCode, { zoom: true, scrollTable: true }), 80);
926
942
  }
927
- else if (act.type === 'prefill_calculator' && currentUser) {
943
+ else if (act.type === 'prefill_calculator' && isUserReadyForOrders()) {
928
944
  $$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === act.tier));
929
945
  if (act.years) $('#inpYears').value = act.years;
930
946
  syncSeats(act.seats);
931
947
  switchStage('addons');
932
948
  }
933
- else if (act.type === 'switch_stage' && act.stage) {
934
- switchStage(act.stage === 'addons' && !currentUser ? 'pricing' : act.stage);
949
+ else if (act.type === 'open_auth') {
950
+ openAuth();
951
+ }
952
+ else if (act.type === 'open_oferta') {
953
+ openOfertaModal();
954
+ }
955
+ else if (act.type === 'switch_stage' && act.stage && act.stage !== 'admin') {
956
+ switchStage(act.stage === 'addons' && !isUserReadyForOrders() ? 'pricing' : act.stage);
935
957
  }
936
958
  }
937
959
  }
@@ -1101,14 +1123,9 @@
1101
1123
  /* ================= Привязка событий ================= */
1102
1124
  function bind() {
1103
1125
  document.addEventListener('click', (e) => {
1104
- const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],[data-cert],[data-pay-order],[data-activate-res],[data-certify-partner],[data-reply-ticket],th[data-sort],#regionsTableBody tr');
1126
+ const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
1105
1127
  if (!el) return;
1106
1128
  if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
1107
- if (el.matches('[data-cert]')) { openCertModal(el.dataset.cert); return; }
1108
- if (el.matches('[data-pay-order]')) { adminConfirmOrder(el.dataset.payOrder); return; }
1109
- if (el.matches('[data-activate-res]')) { adminActivateReservation(el.dataset.activateRes); return; }
1110
- if (el.matches('[data-certify-partner]')) { adminCertifyPartner(el.dataset.certifyPartner); return; }
1111
- if (el.matches('[data-reply-ticket]')) { adminReplyTicket(el.dataset.replyTicket); return; }
1112
1129
  if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
1113
1130
  if (el.matches('th[data-sort]')) {
1114
1131
  SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
@@ -1121,22 +1138,35 @@
1121
1138
  if (el.matches('[data-ask]')) { sendAi(el.dataset.ask); return; }
1122
1139
  if (el.dataset.stage) { e.preventDefault(); switchStage(el.dataset.stage); return; }
1123
1140
  switch (el.dataset.action) {
1124
- case 'auth': if (currentUser) switchStage('addons'); else openAuth(); break;
1141
+ case 'auth': openAuth(); break;
1125
1142
  case 'auth-close': closeAuth(); break;
1126
1143
  case 'login': performLogin(); break;
1127
- case 'addons': e.preventDefault(); if (currentUser) switchStage('addons'); else openAuth(); break;
1144
+ case 'logout': performLogout(); break;
1145
+ case 'open-oferta': e.preventDefault(); openOfertaModal(); break;
1146
+ case 'oferta-close': closeOfertaModal(); break;
1147
+ case 'accept-from-oferta':
1148
+ closeOfertaModal();
1149
+ if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = true;
1150
+ openAuth();
1151
+ break;
1152
+ case 'addons':
1153
+ e.preventDefault();
1154
+ if (isUserReadyForOrders()) switchStage('addons');
1155
+ else requireAuthAndOferta(() => switchStage('addons'));
1156
+ break;
1128
1157
  case 'pick-tier':
1129
1158
  $$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
1130
1159
  recalcAddons();
1131
- if (currentUser) switchStage('addons'); else openAuth();
1160
+ if (isUserReadyForOrders()) switchStage('addons');
1161
+ else requireAuthAndOferta(() => switchStage('addons'));
1132
1162
  break;
1133
- case 'order-pak': submitBasePakOrder(); break;
1134
- case 'order-addons': submitAddonOrder(); break;
1163
+ case 'order-pak': submitBasePakOrder('yookassa'); break;
1164
+ case 'order-pak-invoice': submitBasePakOrder('invoice'); break;
1165
+ case 'order-addons': submitAddonOrder('yookassa'); break;
1166
+ case 'order-addons-invoice': submitAddonOrder('invoice'); break;
1167
+ case 'gen-hub-token': generateCabinetToken(); break;
1135
1168
  case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
1136
1169
  case 'bot-send': sendBot(); break;
1137
- case 'registry': refreshRegistry(); break;
1138
- case 'demo-pak': createDemoPak(); break;
1139
- case 'adm-issue-lic': adminIssueLicense(); break;
1140
1170
  case 'cert-close': $('#certModal')?.classList.remove('open'); break;
1141
1171
  case 'cert-copy':
1142
1172
  navigator.clipboard?.writeText($('#certJsonBox')?.textContent || '');
@@ -1145,22 +1175,24 @@
1145
1175
  case 'ai-send': sendAi(); break;
1146
1176
  }
1147
1177
  });
1148
- $('#authModal').addEventListener('click', (e) => { if (e.target.id === 'authModal') closeAuth(); });
1178
+ $('#authModal')?.addEventListener('click', (e) => { if (e.target.id === 'authModal') closeAuth(); });
1179
+ $('#ofertaModal')?.addEventListener('click', (e) => { if (e.target.id === 'ofertaModal') closeOfertaModal(); });
1149
1180
  $('#certModal')?.addEventListener('click', (e) => { if (e.target.id === 'certModal') $('#certModal').classList.remove('open'); });
1150
1181
  document.addEventListener('keydown', (e) => {
1151
- if (e.key === 'Escape') { closeAuth(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
1182
+ if (e.key === 'Escape') { closeAuth(); closeOfertaModal(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
1152
1183
  });
1153
- $('#authModal').addEventListener('keydown', (e) => { if (e.key === 'Enter') performLogin(); });
1154
- $('#aiInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
1155
- $('#botText').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendBot(); });
1156
- $('#rngAddonSeats').addEventListener('input', (e) => syncSeats(e.target.value));
1157
- $('#inpSeats').addEventListener('input', (e) => syncSeats(e.target.value));
1158
- $('#inpYears').addEventListener('input', recalcAddons);
1159
- $('#inpPakExtraSeats').addEventListener('input', recalcBaseOrder);
1160
- $('#rngPartnerSales').addEventListener('input', recalcPartner);
1161
- $('#inpRegionFilter').addEventListener('input', renderRegionsTable);
1184
+ $('#authModal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') performLogin(); });
1185
+ $('#aiInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
1186
+ $('#botText')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendBot(); });
1187
+ $('#rngAddonSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
1188
+ $('#inpSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
1189
+ $('#inpYears')?.addEventListener('input', recalcAddons);
1190
+ $('#inpPakExtraSeats')?.addEventListener('input', recalcBaseOrder);
1191
+ $('#rngPartnerSales')?.addEventListener('input', recalcPartner);
1192
+ $('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
1162
1193
  bindSeg('segTier', recalcAddons);
1163
1194
  bindSeg('segPakTier', recalcBaseOrder);
1195
+ bindSeg('segPakVariant', recalcBaseOrder);
1164
1196
  window.addEventListener('resize', moveIndicator);
1165
1197
  window.addEventListener('hashchange', () => { const id = location.hash.slice(1); if (id && id !== currentStage) switchStage(id, { scroll: false }); });
1166
1198
  }
@@ -1174,12 +1206,12 @@
1174
1206
  recalcPartner();
1175
1207
  renderRegionsTable();
1176
1208
  initBackdrop();
1177
- const initial = D.initialStage || (location.pathname === '/admin' ? 'admin' : location.hash.slice(1));
1178
- switchStage(initial && initial !== 'addons' ? initial : 'product', { scroll: false });
1209
+ const initial = D.initialStage || location.hash.slice(1);
1210
+ switchStage(initial && initial !== 'addons' && initial !== 'admin' ? initial : 'product', { scroll: false });
1179
1211
  if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
1180
1212
  initMap().then(() => selectRegion(selectedCode || 'RU-SVE'));
1181
1213
  restoreSession();
1182
- refreshRegistry();
1214
+ refreshCatalogRegions();
1183
1215
  }
1184
1216
 
1185
1217
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);