work-license-server 1.0.7 → 1.0.9

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
@@ -67,7 +67,7 @@
67
67
  }
68
68
  if (!$('#stage-' + id)) id = 'product';
69
69
  currentStage = id;
70
- document.body.setAttribute('data-stage', id);
70
+ document.body.setAttribute('data-active-stage', id);
71
71
  $$('.tab').forEach(t => t.classList.toggle('active', t.dataset.stage === id));
72
72
  $$('.stage').forEach(s => s.classList.toggle('active', s.id === 'stage-' + id));
73
73
  moveIndicator();
@@ -80,7 +80,7 @@
80
80
  if (scroll) window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
81
81
  if (window.setBackdropStage) window.setBackdropStage(id);
82
82
  observeReveals();
83
- if (id === 'agents') playConsultantMock();
83
+ if (id === 'product') playConsultantMock();
84
84
  if (id === 'franchise') requestAnimationFrame(() => MAP.ready && applyView());
85
85
  }
86
86
  window.switchStage = switchStage;
@@ -129,15 +129,26 @@
129
129
  card.style.setProperty('--my', (e.clientY - r.top) + 'px');
130
130
  }, { passive: true });
131
131
 
132
- /* ================= Авторизация и подписание публичной оферты (ПЭП) ================= */
133
- function openAuth(hintMsg = '') {
132
+ /* ================= Вход / Регистрация (Email, Мессенджеры, OAuth) ================= */
133
+ function openAuth(hintMsg = '', intentTitle = '') {
134
134
  if ($('#authErrorMsg')) $('#authErrorMsg').textContent = hintMsg || '';
135
+ const intentBox = $('#authIntentNotice');
136
+ if (intentBox) {
137
+ if (intentTitle) {
138
+ intentBox.style.display = '';
139
+ intentBox.innerHTML = `Выбранная позиция: <b>${esc(intentTitle)}</b>.<br>После входа она автоматически появится как заказ в вашем Личном кабинете.`;
140
+ }
141
+ else {
142
+ intentBox.style.display = 'none';
143
+ intentBox.textContent = '';
144
+ }
145
+ }
135
146
  if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = currentUser ? '' : 'none';
136
- $('#authModal').classList.add('open');
147
+ $('#authModal')?.classList.add('open');
137
148
  setTimeout(() => $('#authEmail')?.focus(), 50);
138
149
  }
139
150
  function closeAuth() {
140
- $('#authModal').classList.remove('open');
151
+ $('#authModal')?.classList.remove('open');
141
152
  if ($('#authErrorMsg')) $('#authErrorMsg').textContent = '';
142
153
  }
143
154
  function openOfertaModal() {
@@ -146,59 +157,81 @@
146
157
  function closeOfertaModal() {
147
158
  $('#ofertaModal')?.classList.remove('open');
148
159
  }
149
- function requireAuthAndOferta(actionFn, prefill = {}) {
160
+ function requireAuthAndOferta(actionFn, opts = {}) {
150
161
  if (isUserReadyForOrders()) {
151
162
  if (typeof actionFn === 'function') actionFn();
152
163
  return true;
153
164
  }
154
165
  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('Для оформления заказа, оплаты или резервирования франшизы зарегистрируйтесь и подпишите договор-оферту (ПЭП).');
166
+ openAuth('', opts.intentTitle || '');
159
167
  return false;
160
168
  }
161
169
 
162
- async function performLogin() {
170
+ function resetAuthStep() {
171
+ if ($('#authStepEmail')) $('#authStepEmail').style.display = '';
172
+ if ($('#authStepCode')) $('#authStepCode').style.display = 'none';
173
+ if ($('#authCodeInput')) $('#authCodeInput').value = '';
174
+ if ($('#authErrorMsg')) $('#authErrorMsg').textContent = '';
175
+ setTimeout(() => $('#authEmail')?.focus(), 50);
176
+ }
177
+
178
+ async function sendAuthCode() {
163
179
  const errBox = $('#authErrorMsg');
164
180
  if (errBox) errBox.textContent = '';
165
181
  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
-
182
+ const ofertaChecked = $('#authOfertaCheck') ? Boolean($('#authOfertaCheck').checked) : true;
172
183
  if (!email || !email.includes('@')) {
173
- if (errBox) errBox.textContent = 'Укажите корректный корпоративный email.';
174
- return;
175
- }
176
- if (!companyName || !inn) {
177
- if (errBox) errBox.textContent = 'Укажите наименование организации и ИНН.';
184
+ if (errBox) errBox.textContent = 'Введите ваш корректный email адрес.';
178
185
  return;
179
186
  }
180
187
  if (!ofertaChecked) {
181
- if (errBox) errBox.textContent = 'Для продолжения необходимо ознакомиться с условиями Публичной оферты и поставить отметку о подписании ПЭП.';
188
+ if (errBox) errBox.textContent = 'Подтвердите согласие с условиями Публичного договора-оферты.';
182
189
  return;
183
190
  }
191
+ const btn = $('#btnSendCode');
192
+ if (btn) btn.disabled = true;
193
+ try {
194
+ const res = await api('/api/auth/send-code', { email });
195
+ if ($('#authStepEmail')) $('#authStepEmail').style.display = 'none';
196
+ if ($('#authStepCode')) $('#authStepCode').style.display = '';
197
+ if ($('#authCodeSentNotice')) {
198
+ $('#authCodeSentNotice').textContent = res.message || `Код отправлен на ${email}. Введите его ниже.`;
199
+ }
200
+ if ($('#authCodeInput')) {
201
+ $('#authCodeInput').value = '';
202
+ setTimeout(() => $('#authCodeInput')?.focus(), 50);
203
+ }
204
+ }
205
+ catch (err) {
206
+ if (errBox) errBox.textContent = err.message;
207
+ }
208
+ finally {
209
+ if (btn) btn.disabled = false;
210
+ }
211
+ }
184
212
 
213
+ async function verifyAuthCode() {
214
+ const errBox = $('#authErrorMsg');
215
+ if (errBox) errBox.textContent = '';
216
+ const email = $('#authEmail')?.value.trim() || '';
217
+ const code = $('#authCodeInput')?.value.trim() || '';
218
+ if (!code || code.length < 6) {
219
+ if (errBox) errBox.textContent = 'Введите 6-значный код из письма.';
220
+ return;
221
+ }
222
+ const btn = $('#btnVerifyCode');
223
+ if (btn) btn.disabled = true;
185
224
  try {
186
- const data = await api('/api/auth/register', {
187
- email,
188
- companyName,
189
- inn,
190
- name: signer || companyName,
191
- licenseId,
192
- ofertaAccepted: true,
193
- });
225
+ const data = await api('/api/auth/verify-code', { email, code });
194
226
  currentUser = data;
195
227
  try { localStorage.setItem('odant_work_session', data.sessionKey); } catch { /* приватный режим */ }
228
+ resetAuthStep();
196
229
  onAuthenticated();
197
230
  closeAuth();
198
231
  const nextAction = pendingAuthAction;
199
232
  pendingAuthAction = null;
200
233
  if (typeof nextAction === 'function') {
201
- nextAction();
234
+ await nextAction();
202
235
  }
203
236
  else {
204
237
  switchStage('addons');
@@ -207,17 +240,18 @@
207
240
  catch (err) {
208
241
  if (errBox) errBox.textContent = err.message;
209
242
  }
243
+ finally {
244
+ if (btn) btn.disabled = false;
245
+ }
210
246
  }
211
247
 
212
248
  function performLogout() {
213
249
  currentUser = null;
214
250
  pendingAuthAction = null;
215
251
  try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
216
- $('#tileAddons').style.display = 'none';
217
- $('#btnAuthText').textContent = 'Войти / Регистрация';
252
+ if ($('#tileAddons')) $('#tileAddons').style.display = 'none';
253
+ if ($('#btnAuthText')) $('#btnAuthText').textContent = 'Войти / Регистрация';
218
254
  if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = 'none';
219
- updateOfertaBanners();
220
- updateClientSupportCard();
221
255
  closeAuth();
222
256
  if (currentStage === 'addons') switchStage('product');
223
257
  moveIndicator();
@@ -226,11 +260,7 @@
226
260
  async function restoreSession() {
227
261
  let key = '';
228
262
  try { key = localStorage.getItem('odant_work_session') || ''; } catch { /* приватный режим */ }
229
- if (!key) {
230
- updateOfertaBanners();
231
- updateClientSupportCard();
232
- return;
233
- }
263
+ if (!key) return;
234
264
  try {
235
265
  const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
236
266
  if (data.authenticated && data.user) {
@@ -240,98 +270,74 @@
240
270
  }
241
271
  }
242
272
  catch { /* сессия устарела */ }
243
- updateOfertaBanners();
244
- updateClientSupportCard();
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
273
  }
300
274
 
301
275
  function onAuthenticated() {
302
276
  if (!currentUser) return;
303
- $('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
304
- $('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
305
- if ($('#inpLicId')) $('#inpLicId').value = currentUser.licenseId || '';
277
+ if ($('#tileAddons')) $('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
278
+ if ($('#btnAuthText')) $('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
306
279
  if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = currentUser.hubToken || '';
307
280
  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
281
  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
- if (currentUser.license?.baseActivated && $('#segCabinetBase')) {
324
- $$('#segCabinetBase button').forEach(b => b.classList.toggle('on', b.dataset.base === 'none'));
325
- if (Number($('#inpSeats')?.value) === 0 && $('#inpSeats')) {
326
- $('#inpSeats').value = 5;
327
- if ($('#rngAddonSeats')) $('#rngAddonSeats').value = 5;
328
- }
282
+ if ($('#userBadgeBox')) {
283
+ const orgInfo = currentUser.companyName ? `${esc(currentUser.companyName)}${currentUser.inn ? ' (ИНН ' + esc(currentUser.inn) + ')' : ''} · ` : '';
284
+ $('#userBadgeBox').innerHTML = `${orgInfo}Аккаунт: <b>${esc(currentUser.email)}</b> · <a href="#" data-action="logout" style="color:var(--muted)">Выйти</a>`;
329
285
  }
286
+ renderCabinetOrders();
330
287
  renderCabinetLicenses();
331
- updateOfertaBanners();
332
- updateClientSupportCard();
333
288
  moveIndicator();
334
- recalcAddons();
289
+ }
290
+
291
+ function renderCabinetOrders() {
292
+ const box = $('#cabinetOrdersBox');
293
+ if (!box) return;
294
+ const orders = currentUser?.orders || [];
295
+ if (!orders.length) {
296
+ box.innerHTML = `<div style="padding:18px;border-radius:12px;border:1px dashed var(--border);color:var(--muted);text-align:center;">
297
+ В вашем Личном кабинете пока нет заказов. Перейдите в раздел <a href="#clients" data-stage="clients"><b>«Клиентам»</b></a> (чтобы купить лицензию или ПАК) или <a href="#franchise" data-stage="franchise"><b>«Франшиза»</b></a> (чтобы зарезервировать регион).
298
+ </div>`;
299
+ return;
300
+ }
301
+ box.innerHTML = orders.map(o => {
302
+ const isPaid = o.status === 'paid';
303
+ const title = (o.items && o.items.length)
304
+ ? o.items.map(it => esc(it.title)).join(' + ')
305
+ : esc(o.tierName || o.orderId);
306
+ const dateStr = o.createdAt ? new Date(o.createdAt).toLocaleString('ru-RU') : '';
307
+ const statusBadge = isPaid
308
+ ? `<span class="tag ok">✓ Оплачен</span>`
309
+ : `<span class="tag">Ожидает оплаты</span>`;
310
+ const actionsHtml = isPaid
311
+ ? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
312
+ ${o.invoiceUrl ? `<a class="btn btn-ghost btn-sm" href="${esc(o.invoiceUrl)}" target="_blank" rel="noopener">Открыть счёт-договор (${esc(o.invoiceNumber || o.orderId)}) ↗</a>` : ''}
313
+ </div>`
314
+ : `<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;align-items:center">
315
+ <button class="btn btn-primary btn-sm" data-pay-order="${esc(o.orderId)}" data-pay-method="yookassa">Оплатить через ЮKassa (${rub(o.amountRub)})</button>
316
+ <button class="btn btn-ghost btn-sm" data-invoice-toggle="${esc(o.orderId)}">Счёт-договор (для юр. лиц)</button>
317
+ <button class="btn btn-ghost btn-sm" data-cancel-order="${esc(o.orderId)}" style="color:var(--danger)">Удалить заказ</button>
318
+ </div>
319
+ <div id="invBox-${esc(o.orderId)}" style="display:none;margin-top:12px;padding:12px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.02)">
320
+ <div class="small" style="margin-bottom:8px;font-weight:600">Реквизиты организации для выставления счёта-договора:</div>
321
+ <div class="row2">
322
+ <div class="field"><label>Наименование организации *</label><input type="text" id="invCompany-${esc(o.orderId)}" value="${esc(o.customer?.name || currentUser?.companyName || '')}" placeholder="ООО «Маслоторг»"></div>
323
+ <div class="field"><label>ИНН *</label><input type="text" id="invInn-${esc(o.orderId)}" value="${esc(o.customer?.inn || currentUser?.inn || '')}" placeholder="7701234567" inputmode="numeric"></div>
324
+ </div>
325
+ <button class="btn btn-primary btn-sm" style="margin-top:8px" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Сформировать и открыть счёт-договор →</button>
326
+ </div>`;
327
+ return `<div style="padding:14px 16px;border-radius:12px;border:1px solid var(--border);margin-bottom:10px;">
328
+ <div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
329
+ <div>
330
+ <div style="font-size:12px;color:var(--muted)">Заказ <code>${esc(o.orderId)}</code> · ${esc(dateStr)}</div>
331
+ <div style="font-weight:700;font-size:15px;margin-top:2px">${title}</div>
332
+ </div>
333
+ <div style="text-align:right">
334
+ <div style="font-size:18px;font-weight:800">${rub(o.amountRub)}</div>
335
+ ${statusBadge}
336
+ </div>
337
+ </div>
338
+ ${actionsHtml}
339
+ </div>`;
340
+ }).join('');
335
341
  }
336
342
 
337
343
  function renderCabinetLicenses() {
@@ -339,9 +345,10 @@
339
345
  if (!box) return;
340
346
  const lic = currentUser?.license;
341
347
  const files = lic?.licenseFiles || [];
342
- if (!files.length) {
348
+ const reservations = (currentUser?.reservations || []).filter(r => r.depositPaid);
349
+ if (!files.length && !reservations.length) {
343
350
  box.innerHTML = `<div style="padding:12px 14px;border-radius:10px;border:1px dashed var(--border);color:var(--muted);">
344
- У вас пока нет приобретённых лицензий. Оформите базовую постоянную лицензию (или комплект с машиной) в Шаге 3 ниже — после оплаты файлы лицензий сразу появятся здесь и будут доступны для автоматической загрузки по вашему токену.
351
+ Активных оплаченных лицензий пока нет. После оплаты заказа выше подписанные файлы лицензий (<code>.lic.json</code>) сразу появятся здесь и автоматически загрузятся в ваш локальный WORK по токену владельца.
345
352
  </div>`;
346
353
  return;
347
354
  }
@@ -355,7 +362,11 @@
355
362
  <div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">${esc(expText)}</span></div>
356
363
  </div>`;
357
364
  }).join('');
358
- box.innerHTML = `<div>Всего доступно одновременных сессий: <b style="color:var(--text)">${lic.totalSeats}</b> (${lic.baseSeats} вечных + ${lic.addonSeats} временных)</div>` + rows;
365
+ const resRows = reservations.map(r => `<div style="display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);align-items:center;">
366
+ <div><b>Региональная франшиза: ${esc(r.regionName)}</b><br><code style="font-size:11.5px;color:var(--muted)">${esc(r.reservationId)}</code></div>
367
+ <div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">Залог оплачен</span></div>
368
+ </div>`).join('');
369
+ box.innerHTML = (files.length ? `<div>Всего доступно одновременных сессий: <b style="color:var(--text)">${lic.totalSeats}</b> (${lic.baseSeats} вечных + ${lic.addonSeats} временных)</div>` : '') + rows + resRows;
359
370
  }
360
371
 
361
372
  async function refreshCabinetUser() {
@@ -381,7 +392,7 @@
381
392
  }
382
393
  }
383
394
 
384
- /* ================= Калькуляторы ================= */
395
+ /* ================= Калькуляторы раздела «Клиентам» ================= */
385
396
  function volumeDiscount(seats) {
386
397
  const s = Math.max(0, Math.floor(Number(seats) || 0));
387
398
  return (D.discounts || []).find(d => s >= d.minSeats) || { discountPercent: 0, minSeats: 1 };
@@ -400,136 +411,147 @@
400
411
  const tierById = (id) => (D.tiers || []).find(t => t.id === id) || D.tiers[1];
401
412
  const lines = (rows) => rows.map(([k, v]) => `<div><span>${k}</span><b>${v}</b></div>`).join('');
402
413
 
403
- function recalcAddons() {
404
- const baseMode = $('#segCabinetBase button.on')?.dataset.base || 'license_only';
405
- const includeBasePak = baseMode === 'pak';
406
- const includeBaseLicenseOnly = baseMode === 'license_only';
414
+ function syncClientSeats(v) {
415
+ const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
416
+ const minSeats = variant === 'addons_only' ? 1 : 0;
417
+ const n = Math.max(minSeats, Math.min(2000, Number(v) || 0));
418
+ if ($('#inpPakExtraSeats')) $('#inpPakExtraSeats').value = Math.min(150, n);
419
+ if ($('#inpPakExtraSeatsNum')) $('#inpPakExtraSeatsNum').value = n;
420
+ recalcBaseOrder();
421
+ }
422
+
423
+ function recalcBaseOrder() {
424
+ if (!$('#lblPakTotal')) return;
425
+ const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
426
+ const includeBasePak = variant === 'pak';
427
+ const includeBaseLicenseOnly = variant === 'license_only';
407
428
  const basePrice = includeBasePak
408
429
  ? (D.product?.basePriceRub || 5_000_000)
409
430
  : (includeBaseLicenseOnly ? (D.product?.baseLicensePriceRub || 4_000_000) : 0);
410
431
 
411
- const minSeats = (includeBasePak || includeBaseLicenseOnly) ? 0 : 1;
412
- const seats = Math.max(minSeats, Number($('#inpSeats').value) || 0);
413
- const years = Math.min(5, Math.max(1, Number($('#inpYears').value) || 1));
414
- const tier = tierById(segValue('segTier'));
415
- const disc = volumeDiscount(seats);
416
- const gross = seats * tier.pricePerUserYearRub * years;
417
- const saved = Math.round(gross * disc.discountPercent / 100);
418
- const addonsNet = gross - saved;
419
- const total = basePrice + addonsNet;
420
-
421
- $('#lblTotal').textContent = rub(total);
422
- $('#lblDiscountBadge').textContent = seats > 0 && disc.discountPercent
423
- ? `Скидка за количество −${disc.discountPercent}% · экономия ${rub(saved)}`
424
- : (includeBasePak || includeBaseLicenseOnly ? 'Включает 10 вечных мест и активацию ИИ-агентов' : 'Скидка от количества начинается с 5 мест');
425
-
426
- const breakdownRows = [];
427
- if (includeBasePak) breakdownRows.push(['Базовый комплект (с машиной)', rub(basePrice)]);
428
- if (includeBaseLicenseOnly) breakdownRows.push(['Базовая лицензия (без железа)', rub(basePrice)]);
429
- if (seats > 0) {
430
- breakdownRows.push(['Уровень поддержки доп. мест', `${esc(tier.name)} · ${esc(tier.reactionLabel)}`]);
431
- breakdownRows.push(['Доп. мест × лет', `${seats} × ${years} (${rub(addonsNet)})`]);
432
+ const minSeats = variant === 'addons_only' ? 1 : 0;
433
+ const extra = Math.max(minSeats, Number($('#inpPakExtraSeatsNum')?.value ?? $('#inpPakExtraSeats')?.value) || 0);
434
+ if ($('#inpPakExtraSeatsNum') && Number($('#inpPakExtraSeatsNum').value) !== extra) {
435
+ $('#inpPakExtraSeatsNum').value = extra;
432
436
  }
433
- breakdownRows.push(['Итого мест после покупки', `${(D.product?.baseSeats || 10) + (currentUser?.license?.addonSeats || 0) + seats}`]);
434
- $('#lblBreakdown').innerHTML = lines(breakdownRows);
435
- markLadder('ladderAddons', seats);
436
- }
437
- function syncSeats(v) {
438
- const baseMode = $('#segCabinetBase button.on')?.dataset.base || 'license_only';
439
- const minSeats = baseMode === 'none' ? 1 : 0;
440
- const n = Math.max(minSeats, Math.min(2000, Number(v) || 0));
441
- $('#inpSeats').value = n;
442
- $('#rngAddonSeats').value = Math.min(150, n);
443
- recalcAddons();
444
- }
445
- function recalcBaseOrder() {
446
- const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
447
- const isLicenseOnly = variant === 'license_only';
448
- const basePrice = isLicenseOnly ? (D.product.baseLicensePriceRub || 4_000_000) : D.product.basePriceRub;
449
- const extra = Math.max(0, Number($('#inpPakExtraSeats').value) || 0);
437
+ const years = Math.min(5, Math.max(1, Number($('#inpPakYears')?.value) || 1));
450
438
  const tier = tierById(segValue('segPakTier'));
451
439
  const disc = volumeDiscount(extra);
452
- const gross = extra * tier.pricePerUserYearRub;
453
- const net = gross - Math.round(gross * disc.discountPercent / 100);
454
- $('#lblPakSeats').textContent = extra;
440
+ const gross = extra * tier.pricePerUserYearRub * years;
441
+ const saved = Math.round(gross * disc.discountPercent / 100);
442
+ const net = gross - saved;
443
+
455
444
  $('#lblPakTotal').textContent = rub(basePrice + net);
456
- const rows = [
457
- [isLicenseOnly ? 'Базовая лицензия (без железа)' : 'Комплекс ODANT-WORK (с машиной)', rub(basePrice)],
458
- ['Вечные рабочие места', String(D.product.baseSeats)],
459
- ['Поддержка в комплекте', '1 год · Enterprise (далее — по желанию)'],
460
- ];
461
- if (extra > 0) rows.push([`Доп. места · ${esc(tier.name)}${disc.discountPercent ? ' · −' + disc.discountPercent + '%' : ''}`, `${extra} · ${rub(net)}`]);
445
+ if ($('#lblPakDiscount')) {
446
+ $('#lblPakDiscount').textContent = extra > 0 && disc.discountPercent
447
+ ? `Скидка за количество −${disc.discountPercent}% · экономия ${rub(saved)}`
448
+ : (includeBasePak ? 'Включает сервер, 10 вечных мест и 1 год поддержки Enterprise' : (includeBaseLicenseOnly ? 'Включает 10 вечных мест и активацию ИИ-агентов' : 'Скидка от количества начинается с 5 мест'));
449
+ }
450
+ const rows = [];
451
+ if (includeBasePak) rows.push(['Комплекс ODANT-WORK (с машиной)', rub(basePrice)]);
452
+ if (includeBaseLicenseOnly) rows.push(['Базовая лицензия (без железа)', rub(basePrice)]);
453
+ if (includeBasePak || includeBaseLicenseOnly) rows.push(['Вечные рабочие места', String(D.product.baseSeats)]);
454
+ if (extra > 0) {
455
+ rows.push([`Доп. места · ${esc(tier.name)} (${years} г.)`, `${extra} мест · ${rub(net)}`]);
456
+ }
462
457
  $('#lblPakBreakdown').innerHTML = lines(rows);
458
+ markLadder('ladderPricing', extra);
463
459
  }
464
- async function submitBasePakOrder(paymentMethod = 'yookassa') {
460
+
461
+ async function buyClientConfig() {
462
+ const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
463
+ const includeBasePak = variant === 'pak';
464
+ const includeBaseLicenseOnly = variant === 'license_only';
465
+ const minSeats = variant === 'addons_only' ? 1 : 0;
466
+ const addonSeats = Math.max(minSeats, Number($('#inpPakExtraSeatsNum')?.value ?? $('#inpPakExtraSeats')?.value) || 0);
467
+ const years = Math.min(5, Math.max(1, Number($('#inpPakYears')?.value) || 1));
468
+ const tier = segValue('segPakTier');
469
+
470
+ const intentTitle = includeBasePak
471
+ ? `Комплекс ODANT-WORK с машиной${addonSeats > 0 ? ` + ${addonSeats} доп. мест` : ''}`
472
+ : (includeBaseLicenseOnly
473
+ ? `Базовая лицензия без железа (10 вечных мест)${addonSeats > 0 ? ` + ${addonSeats} доп. мест` : ''}`
474
+ : `Дополнительные временные лицензии (${addonSeats} мест на ${years} г.)`);
475
+
465
476
  if (!isUserReadyForOrders()) {
466
- requireAuthAndOferta(() => submitBasePakOrder(paymentMethod), {
467
- companyName: $('#inpPakOrg')?.value.trim(),
468
- inn: $('#inpPakInn')?.value.trim(),
469
- email: $('#inpPakEmail')?.value.trim(),
470
- });
477
+ requireAuthAndOferta(() => buyClientConfig(), { intentTitle });
471
478
  return;
472
479
  }
473
- const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
474
- const isLicenseOnly = variant === 'license_only';
475
480
  const st = $('#pakOrderStatus');
476
- st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
481
+ if (st) st.textContent = 'Добавляем заказ в ваш Личный кабинет…';
477
482
  try {
478
- const data = await api('/api/orders/checkout', {
483
+ const data = await api('/api/cabinet/orders/draft', {
479
484
  sessionKey: currentUser?.sessionKey || '',
480
- includeBasePak: !isLicenseOnly,
481
- includeBaseLicenseOnly: isLicenseOnly,
482
- paymentMethod,
483
- tier: segValue('segPakTier'),
484
- addonSeats: Number($('#inpPakExtraSeats').value) || 0,
485
- years: 1,
486
- customer: { name: $('#inpPakOrg').value.trim(), inn: $('#inpPakInn').value.trim(), email: $('#inpPakEmail').value.trim() },
485
+ includeBasePak,
486
+ includeBaseLicenseOnly,
487
+ tier,
488
+ addonSeats,
489
+ years,
487
490
  });
488
- if (paymentMethod === 'invoice') {
489
- 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>`;
490
- }
491
- else {
492
- st.innerHTML = `Счёт <b>${esc(data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
491
+ if (data.user) {
492
+ currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
493
+ onAuthenticated();
493
494
  }
494
- refreshCatalogRegions();
495
+ if (st) st.textContent = '';
496
+ switchStage('addons');
495
497
  }
496
- catch (err) { st.textContent = err.message; }
497
- }
498
- async function submitAddonOrder(paymentMethod = 'yookassa') {
499
- if (!isUserReadyForOrders()) {
500
- requireAuthAndOferta(() => submitAddonOrder(paymentMethod));
501
- return;
498
+ catch (err) {
499
+ if (st) st.textContent = err.message;
502
500
  }
503
- const baseMode = $('#segCabinetBase button.on')?.dataset.base || 'license_only';
504
- const includeBasePak = baseMode === 'pak';
505
- const includeBaseLicenseOnly = baseMode === 'license_only';
506
- const minSeats = (includeBasePak || includeBaseLicenseOnly) ? 0 : 1;
507
- const addonSeats = Math.max(minSeats, Number($('#inpSeats').value) || 0);
501
+ }
508
502
 
509
- const st = $('#orderStatus');
510
- st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
503
+ async function payCabinetOrder(orderId, paymentMethod = 'yookassa') {
504
+ if (!orderId) return;
505
+ let companyName = '';
506
+ let inn = '';
507
+ if (paymentMethod === 'invoice') {
508
+ companyName = $(`#invCompany-${orderId}`)?.value.trim() || currentUser?.companyName || '';
509
+ inn = $(`#invInn-${orderId}`)?.value.trim() || currentUser?.inn || '';
510
+ if (!companyName || !inn) {
511
+ const invBox = $(`#invBox-${orderId}`);
512
+ if (invBox) invBox.style.display = '';
513
+ $(`#invCompany-${orderId}`)?.focus();
514
+ return;
515
+ }
516
+ }
511
517
  try {
512
- const data = await api('/api/orders/checkout', {
518
+ const data = await api('/api/cabinet/orders/pay', {
513
519
  sessionKey: currentUser?.sessionKey || '',
514
- includeBasePak,
515
- includeBaseLicenseOnly,
520
+ orderId,
516
521
  paymentMethod,
517
- tier: segValue('segTier'),
518
- addonSeats,
519
- years: Number($('#inpYears').value) || 1,
520
- licenseId: $('#inpLicId').value.trim(),
521
- customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
522
+ companyName,
523
+ inn,
522
524
  });
523
- if (paymentMethod === 'invoice') {
524
- 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>`;
525
+ if (data.user) {
526
+ currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
527
+ onAuthenticated();
525
528
  }
526
- else {
527
- st.innerHTML = `Заказ <b>${esc(data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
529
+ const url = data.invoiceUrl || data.confirmationUrl;
530
+ if (url) window.open(url, '_blank', 'noopener');
531
+ }
532
+ catch (err) {
533
+ alert(err.message);
534
+ }
535
+ }
536
+
537
+ async function cancelCabinetOrder(orderId) {
538
+ if (!orderId) return;
539
+ try {
540
+ const data = await api('/api/cabinet/orders/cancel', {
541
+ sessionKey: currentUser?.sessionKey || '',
542
+ orderId,
543
+ });
544
+ if (data.user) {
545
+ currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
546
+ onAuthenticated();
528
547
  }
529
548
  refreshCatalogRegions();
530
549
  }
531
- catch (err) { st.textContent = err.message; }
550
+ catch (err) {
551
+ alert(err.message);
552
+ }
532
553
  }
554
+
533
555
  async function generateCabinetToken() {
534
556
  if (!currentUser?.sessionKey) return;
535
557
  try {
@@ -570,7 +592,7 @@
570
592
  ];
571
593
  let i = 0;
572
594
  const next = () => {
573
- if (currentStage !== 'agents' || i >= script.length) return;
595
+ if (currentStage !== 'product' || i >= script.length) return;
574
596
  const [who, text, tools] = script[i++];
575
597
  const el = document.createElement('div');
576
598
  if (who === 'v') {
@@ -919,32 +941,33 @@
919
941
  code = code || selectedCode;
920
942
  if (!code) return;
921
943
  selectRegion(code);
944
+ const reg = REGIONS.find(r => codeOf(r) === code);
945
+ const regTitle = reg ? nameOf(reg) : code;
922
946
  if (!isUserReadyForOrders()) {
923
947
  requireAuthAndOferta(() => reserveRegionFranchise(code), {
924
- companyName: $('#inpPartnerCompany')?.value.trim(),
925
- inn: $('#inpPartnerInn')?.value.trim(),
926
- email: $('#inpPartnerEmail')?.value.trim(),
948
+ intentTitle: `Резервирование франшизы (${regTitle})`,
927
949
  });
928
950
  return;
929
951
  }
930
952
  const msg = $('#partnerActionMsg');
931
- msg.textContent = 'Оформляем резерв и счёт на залог…';
953
+ if (msg) msg.textContent = 'Добавляем резерв франшизы в ваш Личный кабинет…';
932
954
  try {
933
- const data = await api('/api/partners/reserve', {
955
+ const data = await api('/api/cabinet/orders/draft', {
934
956
  sessionKey: currentUser?.sessionKey || '',
957
+ kind: 'franchise_reservation',
935
958
  regionCode: code,
936
- companyName: $('#inpPartnerCompany').value.trim(),
937
- inn: $('#inpPartnerInn').value.trim(),
938
- email: $('#inpPartnerEmail').value.trim(),
939
959
  });
940
- mergeRegion(data.regionQuota);
941
- paintMap();
942
- renderRegionsTable();
943
- selectRegion(code);
944
- 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>`;
960
+ if (data.user) {
961
+ currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
962
+ onAuthenticated();
963
+ }
964
+ if (msg) msg.textContent = '';
945
965
  refreshCatalogRegions();
966
+ switchStage('addons');
967
+ }
968
+ catch (err) {
969
+ if (msg) msg.textContent = err.message;
946
970
  }
947
- catch (err) { msg.textContent = err.message; }
948
971
  }
949
972
  window.reserveRegionFranchise = reserveRegionFranchise;
950
973
 
@@ -1200,9 +1223,17 @@
1200
1223
  /* ================= Привязка событий ================= */
1201
1224
  function bind() {
1202
1225
  document.addEventListener('click', (e) => {
1203
- const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
1226
+ const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-pay-order],[data-invoice-toggle],[data-cancel-order],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
1204
1227
  if (!el) return;
1205
1228
  if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
1229
+ if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
1230
+ if (el.matches('[data-invoice-toggle]')) {
1231
+ e.stopPropagation();
1232
+ const box = $(`#invBox-${el.dataset.invoiceToggle}`);
1233
+ if (box) box.style.display = box.style.display === 'none' ? '' : 'none';
1234
+ return;
1235
+ }
1236
+ if (el.matches('[data-cancel-order]')) { e.stopPropagation(); cancelCabinetOrder(el.dataset.cancelOrder); return; }
1206
1237
  if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
1207
1238
  if (el.matches('th[data-sort]')) {
1208
1239
  SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
@@ -1211,14 +1242,24 @@
1211
1242
  return;
1212
1243
  }
1213
1244
  if (el.matches('[data-district]')) { setDistrict(el.dataset.district); return; }
1214
- if (el.matches('[data-bot]')) { $('#botText').value = el.dataset.bot; sendBot(); return; }
1215
1245
  if (el.matches('[data-ask]')) { sendAi(el.dataset.ask); return; }
1216
- if (el.dataset.stage) { e.preventDefault(); switchStage(el.dataset.stage); return; }
1246
+ if (el.dataset.stage && el !== document.body) { e.preventDefault(); switchStage(el.dataset.stage); return; }
1217
1247
  switch (el.dataset.action) {
1218
- case 'auth': openAuth(); break;
1248
+ case 'auth':
1249
+ if (isUserReadyForOrders() && currentStage !== 'addons') {
1250
+ switchStage('addons');
1251
+ }
1252
+ else {
1253
+ pendingAuthAction = null;
1254
+ resetAuthStep();
1255
+ openAuth();
1256
+ }
1257
+ break;
1219
1258
  case 'auth-close': closeAuth(); break;
1220
- case 'login': performLogin(); break;
1221
- case 'logout': performLogout(); break;
1259
+ case 'send-code': sendAuthCode(); break;
1260
+ case 'verify-code': verifyAuthCode(); break;
1261
+ case 'reset-auth-step': resetAuthStep(); break;
1262
+ case 'logout': e.preventDefault(); performLogout(); break;
1222
1263
  case 'open-oferta': e.preventDefault(); openOfertaModal(); break;
1223
1264
  case 'oferta-close': closeOfertaModal(); break;
1224
1265
  case 'accept-from-oferta':
@@ -1232,20 +1273,18 @@
1232
1273
  else requireAuthAndOferta(() => switchStage('addons'));
1233
1274
  break;
1234
1275
  case 'pick-tier':
1235
- $$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
1236
- recalcAddons();
1237
- if (isUserReadyForOrders()) switchStage('addons');
1238
- else requireAuthAndOferta(() => switchStage('addons'));
1276
+ $$('#segPakTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
1277
+ if ($('#segPakVariant')) {
1278
+ $$('#segPakVariant button').forEach(b => b.classList.toggle('on', b.dataset.variant === 'addons_only'));
1279
+ }
1280
+ syncClientSeats(5);
1281
+ buyClientConfig();
1239
1282
  break;
1240
- case 'order-pak': submitBasePakOrder('yookassa'); break;
1241
- case 'order-pak-invoice': submitBasePakOrder('invoice'); break;
1242
- case 'order-addons': submitAddonOrder('yookassa'); break;
1243
- case 'order-addons-invoice': submitAddonOrder('invoice'); break;
1283
+ case 'buy-client-config': buyClientConfig(); break;
1244
1284
  case 'gen-hub-token': generateCabinetToken(); break;
1245
1285
  case 'copy-hub-token': copyCabinetToken(); break;
1246
1286
  case 'refresh-cabinet': refreshCabinetUser(); break;
1247
1287
  case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
1248
- case 'bot-send': sendBot(); break;
1249
1288
  case 'cert-close': $('#certModal')?.classList.remove('open'); break;
1250
1289
  case 'cert-copy':
1251
1290
  navigator.clipboard?.writeText($('#certJsonBox')?.textContent || '');
@@ -1260,19 +1299,20 @@
1260
1299
  document.addEventListener('keydown', (e) => {
1261
1300
  if (e.key === 'Escape') { closeAuth(); closeOfertaModal(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
1262
1301
  });
1263
- $('#authModal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') performLogin(); });
1302
+ $('#authModal')?.addEventListener('keydown', (e) => {
1303
+ if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') {
1304
+ if ($('#authStepCode')?.style.display !== 'none') verifyAuthCode();
1305
+ else sendAuthCode();
1306
+ }
1307
+ });
1264
1308
  $('#aiInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
1265
- $('#botText')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendBot(); });
1266
- $('#rngAddonSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
1267
- $('#inpSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
1268
- $('#inpYears')?.addEventListener('input', recalcAddons);
1269
- $('#inpPakExtraSeats')?.addEventListener('input', recalcBaseOrder);
1309
+ $('#inpPakExtraSeats')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
1310
+ $('#inpPakExtraSeatsNum')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
1311
+ $('#inpPakYears')?.addEventListener('input', recalcBaseOrder);
1270
1312
  $('#rngPartnerSales')?.addEventListener('input', recalcPartner);
1271
1313
  $('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
1272
- bindSeg('segTier', recalcAddons);
1273
- bindSeg('segCabinetBase', () => { syncSeats($('#inpSeats')?.value || 0); });
1274
1314
  bindSeg('segPakTier', recalcBaseOrder);
1275
- bindSeg('segPakVariant', recalcBaseOrder);
1315
+ bindSeg('segPakVariant', () => syncClientSeats($('#inpPakExtraSeatsNum')?.value || 0));
1276
1316
  window.addEventListener('message', (e) => {
1277
1317
  if (e.data?.type === 'odant:order-paid') {
1278
1318
  refreshCabinetUser();
@@ -1286,7 +1326,6 @@
1286
1326
  applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
1287
1327
  bind();
1288
1328
  renderOrbit();
1289
- recalcAddons();
1290
1329
  recalcBaseOrder();
1291
1330
  recalcPartner();
1292
1331
  renderRegionsTable();