work-license-server 1.0.13 → 1.0.15
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/admin.js +602 -299
- package/assets/landing.js +541 -69
- package/package.json +1 -1
- package/src/admin.js +279 -182
- package/src/crypto.js +32 -3
- package/src/landing.js +38 -30
- package/src/server.js +121 -4
- package/src/store.js +810 -58
package/assets/landing.js
CHANGED
|
@@ -277,6 +277,8 @@
|
|
|
277
277
|
catch { /* сессия устарела */ }
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
let franchiseCountdownTimer = null;
|
|
281
|
+
|
|
280
282
|
function onAuthenticated() {
|
|
281
283
|
if (!currentUser) return;
|
|
282
284
|
if ($('#tileAddons')) $('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
|
|
@@ -288,7 +290,6 @@
|
|
|
288
290
|
} catch { /* ignore */ }
|
|
289
291
|
}
|
|
290
292
|
if ($('#btnAuthText')) $('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
|
|
291
|
-
if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = currentUser.hubToken || '';
|
|
292
293
|
if ($('#authEmail')) $('#authEmail').value = currentUser.email || '';
|
|
293
294
|
if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = Boolean(currentUser.ofertaAccepted);
|
|
294
295
|
if ($('#userBadgeBox')) {
|
|
@@ -296,90 +297,376 @@
|
|
|
296
297
|
const bossBadge = currentUser.isAdmin ? `<span class="tag ok" style="margin-right:6px">BOSS · Администратор</span> <a href="/admin" style="font-weight:700;margin-right:8px">Открыть Админ-панель →</a> · ` : '';
|
|
297
298
|
$('#userBadgeBox').innerHTML = `${bossBadge}${orgInfo}Аккаунт: <b>${esc(currentUser.email)}</b> · <a href="#" data-action="logout" style="color:var(--muted)">Выйти</a>`;
|
|
298
299
|
}
|
|
299
|
-
|
|
300
|
-
|
|
300
|
+
syncClientServerPicker();
|
|
301
|
+
renderCabinetServers();
|
|
302
|
+
renderCabinetFranchises();
|
|
301
303
|
moveIndicator();
|
|
302
304
|
}
|
|
303
305
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
+
function getUserServersList() {
|
|
307
|
+
if (Array.isArray(currentUser?.servers) && currentUser.servers.length) {
|
|
308
|
+
return currentUser.servers;
|
|
309
|
+
}
|
|
310
|
+
if (currentUser?.license) {
|
|
311
|
+
return [{
|
|
312
|
+
...currentUser.license,
|
|
313
|
+
serverName: currentUser.license.serverName || 'Сервер WORK #1',
|
|
314
|
+
hubToken: currentUser.license.hubToken || currentUser.hubToken || '',
|
|
315
|
+
orders: (currentUser.orders || []).filter(o => o.kind !== 'franchise_reservation' && o.kind !== 'franchise_installment'),
|
|
316
|
+
}];
|
|
317
|
+
}
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function syncClientServerPicker(preferredServerId = '') {
|
|
322
|
+
const wrap = $('#clientServerPickerWrap');
|
|
323
|
+
const sel = $('#selClientTargetServer');
|
|
324
|
+
if (!wrap || !sel) return;
|
|
325
|
+
const servers = getUserServersList();
|
|
326
|
+
if (!currentUser || !servers.length) {
|
|
327
|
+
wrap.style.display = 'none';
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
wrap.style.display = '';
|
|
331
|
+
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
332
|
+
const isBaseBuy = variant === 'pak' || variant === 'license_only';
|
|
333
|
+
const prevVal = preferredServerId || sel.value || '';
|
|
334
|
+
|
|
335
|
+
const options = [];
|
|
336
|
+
for (const s of servers) {
|
|
337
|
+
const statusHint = s.baseActivated
|
|
338
|
+
? `базовая активна · ${s.totalSeats} мест`
|
|
339
|
+
: 'без базовой лицензии';
|
|
340
|
+
options.push(`<option value="${esc(s.licenseId)}">${esc(s.serverName)} (${esc(s.licenseId)} · ${statusHint})</option>`);
|
|
341
|
+
}
|
|
342
|
+
if (isBaseBuy) {
|
|
343
|
+
options.push(`<option value="NEW">+ Создать новый сервер в реестре</option>`);
|
|
344
|
+
}
|
|
345
|
+
sel.innerHTML = options.join('');
|
|
346
|
+
|
|
347
|
+
if (prevVal && Array.from(sel.options).some(o => o.value === prevVal)) {
|
|
348
|
+
sel.value = prevVal;
|
|
349
|
+
}
|
|
350
|
+
else if (isBaseBuy) {
|
|
351
|
+
const unactivated = servers.find(s => !s.baseActivated);
|
|
352
|
+
sel.value = unactivated ? unactivated.licenseId : 'NEW';
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
sel.value = servers[0]?.licenseId || '';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function renderCabinetServers() {
|
|
360
|
+
const box = $('#cabinetServersRegistry');
|
|
306
361
|
if (!box) return;
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
|
|
362
|
+
const servers = getUserServersList();
|
|
363
|
+
if ($('#badgeServersCount')) $('#badgeServersCount').textContent = String(servers.length);
|
|
364
|
+
|
|
365
|
+
if (!servers.length) {
|
|
366
|
+
box.innerHTML = `<div style="padding:22px;border-radius:12px;border:1px dashed var(--border);color:var(--muted);text-align:center;">
|
|
367
|
+
В вашем реестре пока нет добавленных серверов WORK. Введите название сервера выше и нажмите <b>«+ Добавить сервер (устройство)»</b>.
|
|
311
368
|
</div>`;
|
|
312
369
|
return;
|
|
313
370
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
?
|
|
322
|
-
:
|
|
323
|
-
|
|
371
|
+
|
|
372
|
+
box.innerHTML = servers.map((s, idx) => {
|
|
373
|
+
const lid = s.licenseId;
|
|
374
|
+
const name = s.serverName || `Сервер WORK #${idx + 1}`;
|
|
375
|
+
const tok = s.hubToken || currentUser?.hubToken || '';
|
|
376
|
+
const hwidBound = Boolean(s.hwidBound || s.hardware?.hwidBound || s.hardware?.nodeFingerprint);
|
|
377
|
+
const hwidText = hwidBound
|
|
378
|
+
? `Железо привязано: <code>${esc(s.hardware?.hwid || '')}</code>${s.hardware?.nodeFingerprint ? ` · узел <code>${esc(s.hardware.nodeFingerprint.slice(0, 16))}…</code>` : ''}`
|
|
379
|
+
: 'Ожидает привязки токена в локальном WORK (раздел WORK → Лицензия)';
|
|
380
|
+
|
|
381
|
+
const baseBadge = s.baseActivated
|
|
382
|
+
? `<span class="tag ok">✓ Базовая активна · ${s.baseSeats || 10} вечных мест + ИИ-агенты</span>`
|
|
383
|
+
: `<span class="tag">Базовая не активирована (1 сессия BOSS · ИИ отключён)</span>`;
|
|
384
|
+
|
|
385
|
+
const files = s.licenseFiles || [];
|
|
386
|
+
const activeAddons = s.activeAddons || [];
|
|
387
|
+
const serverOrders = (s.orders || []).filter(o => o.status !== 'canceled');
|
|
388
|
+
const pendingOrders = serverOrders.filter(o => o.status !== 'paid');
|
|
389
|
+
const paidOrders = serverOrders.filter(o => o.status === 'paid');
|
|
390
|
+
const canDelete = !s.baseActivated && !activeAddons.length && !paidOrders.length;
|
|
391
|
+
|
|
392
|
+
const baseActionsHtml = !s.baseActivated
|
|
324
393
|
? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
325
|
-
|
|
394
|
+
<button class="btn btn-primary btn-sm" data-buy-for-server="${esc(lid)}" data-buy-variant="license_only">Купить базовую лицензию (${rub(D.product?.baseLicensePriceRub || 4_000_000)})</button>
|
|
395
|
+
<button class="btn btn-ghost btn-sm" data-buy-for-server="${esc(lid)}" data-buy-variant="pak">Заказать ПАК с сервером (${rub(D.product?.basePriceRub || 5_000_000)})</button>
|
|
326
396
|
</div>`
|
|
327
|
-
:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
397
|
+
: '';
|
|
398
|
+
|
|
399
|
+
const addonsListHtml = activeAddons.length
|
|
400
|
+
? `<div style="margin-top:8px;display:flex;flex-direction:column;gap:6px">
|
|
401
|
+
${activeAddons.map(sub => {
|
|
402
|
+
const exp = sub.expiresAt ? new Date(sub.expiresAt).toLocaleDateString('ru-RU') : '—';
|
|
403
|
+
return `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-radius:8px;background:rgba(255,255,255,.03);border:1px solid var(--border)">
|
|
404
|
+
<span><b>+${sub.seats} конк. мест</b> · уровень <b>${esc(sub.tierName || sub.tier)}</b> (${sub.years || 1} г.)</span>
|
|
405
|
+
<span class="small muted">действует до ${esc(exp)}</span>
|
|
406
|
+
</div>`;
|
|
407
|
+
}).join('')}
|
|
408
|
+
</div>`
|
|
409
|
+
: `<div class="small muted" style="margin-top:6px">Дополнительные временные места не подключены.</div>`;
|
|
410
|
+
|
|
411
|
+
const filesHtml = files.length
|
|
412
|
+
? `<div style="margin-top:10px;padding-top:10px;border-top:1px dashed var(--border)">
|
|
413
|
+
<div class="small muted" style="margin-bottom:6px">Выпущенные подписанные файлы лицензий этого сервера (автоматически загружаются в <code>#system</code> по токену):</div>
|
|
414
|
+
<div style="display:flex;flex-direction:column;gap:6px">
|
|
415
|
+
${files.map((f, fIdx) => {
|
|
416
|
+
const lf = f.licenseFile || f;
|
|
417
|
+
const isBase = lf.kind === 'base' || lf.perpetual;
|
|
418
|
+
const label = isBase ? 'Базовая вечная лицензия (10 мест + ИИ)' : `Доп. лицензия (+${lf.seats} мест · ${String(lf.tier || '').toUpperCase()})`;
|
|
419
|
+
return `<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;font-size:12.5px">
|
|
420
|
+
<span><b>${esc(label)}</b> · <code>${esc(f.fileName || lf.fileId)}</code></span>
|
|
421
|
+
<button class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-download-lic="${esc(lid)}" data-file-idx="${fIdx}">Скачать .lic.json</button>
|
|
422
|
+
</div>`;
|
|
423
|
+
}).join('')}
|
|
337
424
|
</div>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
425
|
+
</div>`
|
|
426
|
+
: '';
|
|
427
|
+
|
|
428
|
+
const pendingOrdersHtml = pendingOrders.length
|
|
429
|
+
? `<div style="margin-top:14px;padding-top:12px;border-top:1px solid var(--border)">
|
|
430
|
+
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--warn);margin-bottom:8px">Ожидают оплаты по этому серверу (${pendingOrders.length})</div>
|
|
431
|
+
${pendingOrders.map(o => {
|
|
432
|
+
const title = (o.items && o.items.length) ? o.items.map(it => esc(it.title)).join(' + ') : esc(o.tierName || o.orderId);
|
|
433
|
+
return `<div style="padding:12px 14px;border-radius:10px;border:1px solid var(--border);background:rgba(245,158,11,.05);margin-bottom:8px">
|
|
434
|
+
<div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
|
|
435
|
+
<div>
|
|
436
|
+
<div class="small muted">Заказ <code>${esc(o.orderId)}</code></div>
|
|
437
|
+
<div style="font-weight:700;font-size:14px;margin-top:2px">${title}</div>
|
|
438
|
+
</div>
|
|
439
|
+
<div style="font-size:17px;font-weight:800">${rub(o.amountRub)}</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;align-items:center">
|
|
442
|
+
<button class="btn btn-primary btn-sm" data-pay-order="${esc(o.orderId)}" data-pay-method="yookassa">Оплатить через ЮKassa (${rub(o.amountRub)})</button>
|
|
443
|
+
<button class="btn btn-ghost btn-sm" data-invoice-toggle="${esc(o.orderId)}">Счёт-договор (для юр. лиц)</button>
|
|
444
|
+
<button class="btn btn-ghost btn-sm" data-cancel-order="${esc(o.orderId)}" style="color:var(--danger)">Отменить заказ</button>
|
|
445
|
+
</div>
|
|
446
|
+
<div id="invBox-${esc(o.orderId)}" style="display:none;margin-top:10px;padding:12px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.02)">
|
|
447
|
+
<div class="small" style="margin-bottom:8px;font-weight:600">Реквизиты юр. лица для счёта-договора:</div>
|
|
448
|
+
<div class="row2">
|
|
449
|
+
<div class="field"><label>Наименование организации *</label><input type="text" id="invCompany-${esc(o.orderId)}" value="${esc(o.customer?.name || currentUser?.companyName || '')}" placeholder="ООО «Маслоторг»"></div>
|
|
450
|
+
<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>
|
|
451
|
+
</div>
|
|
452
|
+
<button class="btn btn-primary btn-sm" style="margin-top:8px" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Сформировать и открыть счёт-договор →</button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>`;
|
|
455
|
+
}).join('')}
|
|
456
|
+
</div>`
|
|
457
|
+
: '';
|
|
458
|
+
|
|
459
|
+
return `<div style="padding:18px 20px;border-radius:14px;border:1px solid var(--border);background:rgba(255,255,255,.015)">
|
|
460
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;padding-bottom:12px;border-bottom:1px solid var(--border)">
|
|
342
461
|
<div>
|
|
343
|
-
<div style="
|
|
344
|
-
|
|
462
|
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
463
|
+
<h4 class="h3" style="margin:0;font-size:18px">${esc(name)}</h4>
|
|
464
|
+
<code style="font-size:12px;padding:2px 8px;border-radius:6px;background:rgba(255,255,255,.06)">${esc(lid)}</code>
|
|
465
|
+
${baseBadge}
|
|
466
|
+
</div>
|
|
467
|
+
<div class="small muted" style="margin-top:6px">${hwidText}</div>
|
|
345
468
|
</div>
|
|
346
|
-
<div style="
|
|
347
|
-
<
|
|
348
|
-
${
|
|
469
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
470
|
+
<span class="pill" style="padding:4px 12px;font-size:12.5px">Сессий всего: <b>${s.totalSeats}</b> (${s.baseSeats || 0} вечн. + ${s.addonSeats || 0} доп.)</span>
|
|
471
|
+
<button class="btn btn-ghost btn-sm" data-rename-server="${esc(lid)}" data-current-name="${esc(name)}">Переименовать</button>
|
|
472
|
+
${canDelete ? `<button class="btn btn-ghost btn-sm" data-delete-server="${esc(lid)}" style="color:var(--danger)">Удалить</button>` : ''}
|
|
349
473
|
</div>
|
|
350
474
|
</div>
|
|
351
|
-
|
|
475
|
+
|
|
476
|
+
<div class="split" style="margin-top:14px;gap:16px">
|
|
477
|
+
<!-- Токен этого сервера -->
|
|
478
|
+
<div style="padding:12px 14px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.02)">
|
|
479
|
+
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--brand-2);margin-bottom:4px">Токен привязки этого сервера (owk1...)</div>
|
|
480
|
+
<p class="small muted" style="margin:0 0 8px">Вставьте этот токен в <b>WORK → Лицензия</b> на данном сервере. Токен привяжет устройство и автоматически скачает все его лицензии в <code>#system</code>.</p>
|
|
481
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
482
|
+
<input type="text" id="srvTok-${esc(lid)}" readonly value="${esc(tok)}" style="flex:1;min-width:180px;font-family:monospace;font-size:12.5px;padding:7px 10px">
|
|
483
|
+
<button class="btn btn-primary btn-sm" data-copy-server-token="${esc(lid)}">Скопировать токен</button>
|
|
484
|
+
<button class="btn btn-ghost btn-sm" data-regen-server-token="${esc(lid)}">Перевыпустить</button>
|
|
485
|
+
</div>
|
|
486
|
+
<div class="small" id="srvTokStatus-${esc(lid)}" style="color:var(--ok);min-height:16px;margin-top:4px"></div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Лицензии этого сервера -->
|
|
490
|
+
<div style="padding:12px 14px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.02)">
|
|
491
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap">
|
|
492
|
+
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--brand-2)">Лицензии сервера (базовая и доп.)</div>
|
|
493
|
+
<button class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-buy-for-server="${esc(lid)}" data-buy-variant="addons_only">+ Докупить рабочие места / SLA</button>
|
|
494
|
+
</div>
|
|
495
|
+
<div class="small" style="margin-top:6px">
|
|
496
|
+
<b>Базовая лицензия:</b> ${s.baseActivated ? '<span style="color:var(--ok)">Активирована (10 вечных мест + ИИ-агенты)</span>' : '<span style="color:var(--muted)">Не активирована</span>'}
|
|
497
|
+
</div>
|
|
498
|
+
${baseActionsHtml}
|
|
499
|
+
<div class="small" style="margin-top:8px"><b>Дополнительные годовые лицензии:</b></div>
|
|
500
|
+
${addonsListHtml}
|
|
501
|
+
${filesHtml}
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
${pendingOrdersHtml}
|
|
352
506
|
</div>`;
|
|
353
507
|
}).join('');
|
|
354
508
|
}
|
|
355
509
|
|
|
356
|
-
function
|
|
357
|
-
const
|
|
510
|
+
function fmtCountdown(sec) {
|
|
511
|
+
const s = Math.max(0, Math.floor(Number(sec) || 0));
|
|
512
|
+
const m = Math.floor(s / 60);
|
|
513
|
+
const r = s % 60;
|
|
514
|
+
return `${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function startFranchiseCountdowns() {
|
|
518
|
+
if (franchiseCountdownTimer) {
|
|
519
|
+
clearInterval(franchiseCountdownTimer);
|
|
520
|
+
franchiseCountdownTimer = null;
|
|
521
|
+
}
|
|
522
|
+
const nodes = $$('[data-deposit-deadline]');
|
|
523
|
+
if (!nodes.length) return;
|
|
524
|
+
franchiseCountdownTimer = setInterval(() => {
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
let expiredAny = false;
|
|
527
|
+
for (const el of $$('[data-deposit-deadline]')) {
|
|
528
|
+
const deadline = Number(el.dataset.depositDeadline || 0);
|
|
529
|
+
const remSec = Math.max(0, Math.floor((deadline - now) / 1000));
|
|
530
|
+
el.textContent = fmtCountdown(remSec);
|
|
531
|
+
if (remSec <= 0) expiredAny = true;
|
|
532
|
+
}
|
|
533
|
+
if (expiredAny) {
|
|
534
|
+
clearInterval(franchiseCountdownTimer);
|
|
535
|
+
franchiseCountdownTimer = null;
|
|
536
|
+
refreshCabinetUser();
|
|
537
|
+
refreshCatalogRegions();
|
|
538
|
+
}
|
|
539
|
+
}, 1000);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderCabinetFranchises() {
|
|
543
|
+
const box = $('#cabinetFranchisesRegistry');
|
|
358
544
|
if (!box) return;
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
545
|
+
const franchises = currentUser?.franchises || currentUser?.reservations || [];
|
|
546
|
+
const activeList = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
547
|
+
if ($('#badgeFranchisesCount')) $('#badgeFranchisesCount').textContent = String(activeList.length);
|
|
548
|
+
|
|
549
|
+
if (!activeList.length) {
|
|
550
|
+
box.innerHTML = `<div style="padding:22px;border-radius:12px;border:1px dashed var(--border);color:var(--muted);text-align:center;">
|
|
551
|
+
У вас пока нет зарезервированных или действующих региональных франшиз. Нажмите <b>«Зарезервировать франшизу на карте РФ»</b> выше, чтобы выбрать субъект РФ.
|
|
365
552
|
</div>`;
|
|
553
|
+
startFranchiseCountdowns();
|
|
366
554
|
return;
|
|
367
555
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
556
|
+
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
box.innerHTML = activeList.map(f => {
|
|
559
|
+
const rid = f.reservationId;
|
|
560
|
+
const depositRub = Number(f.nonRefundableDepositRub || 100_000);
|
|
561
|
+
const totalFeeRub = Number(f.franchiseFeeRub || 500_000);
|
|
562
|
+
const totalPaidRub = Number(f.totalPaidRub ?? (f.depositPaid ? depositRub : 0));
|
|
563
|
+
const remainingRub = Number(f.remainingFranchiseFeeRub ?? Math.max(0, totalFeeRub - totalPaidRub));
|
|
564
|
+
const schedule = Array.isArray(f.paymentSchedule) ? f.paymentSchedule : [];
|
|
565
|
+
const paidMonths = Number(f.paidMonthsCount ?? schedule.filter(x => x.status === 'paid').length);
|
|
566
|
+
const pct = totalFeeRub > 0 ? Math.min(100, Math.round((totalPaidRub / totalFeeRub) * 100)) : 0;
|
|
567
|
+
|
|
568
|
+
const deadline = Number(f.depositDeadlineAt || ((f.createdAt || now) + 15 * 60_000));
|
|
569
|
+
const remSec = Math.max(0, Math.floor((deadline - now) / 1000));
|
|
570
|
+
|
|
571
|
+
const statusBadge = !f.depositPaid
|
|
572
|
+
? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Ожидает оплаты залога · <b data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</b></span>`
|
|
573
|
+
: (remainingRub <= 0
|
|
574
|
+
? `<span class="tag ok">✓ Франшиза полностью оплачена (12/12)</span>`
|
|
575
|
+
: `<span class="tag ok">✓ Залог оплачен · Погашение по графику (${paidMonths}/12 мес.)</span>`);
|
|
576
|
+
|
|
577
|
+
const depositAlertHtml = !f.depositPaid
|
|
578
|
+
? `<div style="margin-top:12px;padding:14px 16px;border-radius:12px;border:1px solid rgba(245,158,11,.45);background:rgba(245,158,11,.08);display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap">
|
|
579
|
+
<div>
|
|
580
|
+
<div style="font-weight:700;color:var(--warn)">Требуется оплата начального взноса (${rub(depositRub)}) в течение 15 минут</div>
|
|
581
|
+
<div class="small muted" style="margin-top:2px">Осталось времени: <b style="color:var(--text);font-family:monospace;font-size:14px" data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</b>. Если оплата через ЮKassa не поступит вовремя, резерв региона автоматически отменится.</div>
|
|
582
|
+
</div>
|
|
583
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
584
|
+
<button class="btn btn-primary btn-sm" data-pay-order="${esc(f.orderId)}" data-pay-method="yookassa">Оплатить начальный взнос ${rub(depositRub)} (ЮKassa)</button>
|
|
585
|
+
<button class="btn btn-ghost btn-sm" data-cancel-franchise="${esc(rid)}" style="color:var(--danger)">Отменить резерв</button>
|
|
586
|
+
</div>
|
|
587
|
+
</div>`
|
|
588
|
+
: '';
|
|
589
|
+
|
|
590
|
+
const nextInst = f.nextInstallment || schedule.find(x => x.status !== 'paid');
|
|
591
|
+
const scheduleControlsHtml = (f.depositPaid && remainingRub > 0)
|
|
592
|
+
? `<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
593
|
+
${nextInst ? `<button class="btn btn-primary btn-sm" data-pay-installment="${esc(rid)}" data-month="${nextInst.monthNumber}">Оплатить платёж №${nextInst.monthNumber} (${rub(nextInst.amountRub)} · ЮKassa)</button>` : ''}
|
|
594
|
+
<button class="btn btn-ghost btn-sm" data-pay-installment="${esc(rid)}" data-early="1">Досрочно погасить весь остаток (${rub(remainingRub)})</button>
|
|
595
|
+
</div>`
|
|
596
|
+
: '';
|
|
597
|
+
|
|
598
|
+
const scheduleRowsHtml = schedule.map(inst => {
|
|
599
|
+
const isPaid = inst.status === 'paid';
|
|
600
|
+
const dueStr = inst.dueDate ? new Date(inst.dueDate).toLocaleDateString('ru-RU') : '—';
|
|
601
|
+
const paidStr = inst.paidAt ? new Date(inst.paidAt).toLocaleDateString('ru-RU') : '';
|
|
602
|
+
let stHtml = '<span class="tag">По графику</span>';
|
|
603
|
+
if (isPaid) {
|
|
604
|
+
stHtml = `<span class="tag ok">✓ Оплачен${paidStr ? ' ' + esc(paidStr) : ''}</span>`;
|
|
605
|
+
}
|
|
606
|
+
else if (inst.displayStatus === 'overdue') {
|
|
607
|
+
stHtml = '<span class="tag bad">Просрочен</span>';
|
|
608
|
+
}
|
|
609
|
+
else if (inst.displayStatus === 'next') {
|
|
610
|
+
stHtml = '<span class="tag" style="color:var(--brand-2)">Ближайший к оплате</span>';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const actHtml = (!isPaid && f.depositPaid)
|
|
614
|
+
? `<div style="display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap">
|
|
615
|
+
<button class="btn btn-primary btn-sm" style="padding:4px 10px;font-size:12px" data-pay-installment="${esc(rid)}" data-month="${inst.monthNumber}" data-pay-method="yookassa">Оплатить ЮKassa</button>
|
|
616
|
+
<button class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-pay-installment="${esc(rid)}" data-month="${inst.monthNumber}" data-pay-method="invoice">Счёт</button>
|
|
617
|
+
</div>`
|
|
618
|
+
: (isPaid ? `<span class="small muted">Закрыт</span>` : `<span class="small muted">После оплаты залога</span>`);
|
|
619
|
+
|
|
620
|
+
return `<tr>
|
|
621
|
+
<td><b>Месяц ${inst.monthNumber} / 12</b></td>
|
|
622
|
+
<td>${esc(dueStr)}</td>
|
|
623
|
+
<td class="num"><b>${rub(inst.amountRub)}</b></td>
|
|
624
|
+
<td>${stHtml}</td>
|
|
625
|
+
<td style="text-align:right">${actHtml}</td>
|
|
626
|
+
</tr>`;
|
|
627
|
+
}).join('');
|
|
628
|
+
|
|
629
|
+
return `<div style="padding:18px 20px;border-radius:14px;border:1px solid var(--border);background:rgba(255,255,255,.015)">
|
|
630
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;padding-bottom:12px;border-bottom:1px solid var(--border)">
|
|
631
|
+
<div>
|
|
632
|
+
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
|
633
|
+
<h4 class="h3" style="margin:0;font-size:18px">${esc(f.regionName)}</h4>
|
|
634
|
+
<span class="tag">${esc(f.district || f.regionCode)}</span>
|
|
635
|
+
<code style="font-size:12px;color:var(--muted)">${esc(rid)}</code>
|
|
636
|
+
${statusBadge}
|
|
637
|
+
</div>
|
|
638
|
+
<div class="small muted" style="margin-top:6px">
|
|
639
|
+
Паушальный взнос региона: <b>${rub(totalFeeRub)}</b> · Начальный взнос (залог): <b>${rub(depositRub)}</b> (${f.depositPaid ? 'оплачен' : 'ожидает оплаты'}) · Погашено всего: <b>${rub(totalPaidRub)}</b> · Остаток: <b>${rub(remainingRub)}</b>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
${scheduleControlsHtml}
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
${depositAlertHtml}
|
|
646
|
+
|
|
647
|
+
<div style="margin-top:14px">
|
|
648
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap">
|
|
649
|
+
<span class="small" style="font-weight:700">Годовой график погашения платежей (12 месяцев, платёж раз в месяц)</span>
|
|
650
|
+
<span class="small muted">Погашено ${pct}% (${rub(totalPaidRub)} из ${rub(totalFeeRub)})</span>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="bar" style="width:100%;height:8px;margin-bottom:12px"><i style="--p:${pct}%"></i></div>
|
|
653
|
+
<div class="table-wrap" style="max-height:340px">
|
|
654
|
+
<table class="t">
|
|
655
|
+
<thead><tr>
|
|
656
|
+
<th>Период</th>
|
|
657
|
+
<th>Дата платежа</th>
|
|
658
|
+
<th class="num">Сумма взноса</th>
|
|
659
|
+
<th>Статус</th>
|
|
660
|
+
<th style="text-align:right">Действие</th>
|
|
661
|
+
</tr></thead>
|
|
662
|
+
<tbody>${scheduleRowsHtml}</tbody>
|
|
663
|
+
</table>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
376
666
|
</div>`;
|
|
377
667
|
}).join('');
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">Залог оплачен</span></div>
|
|
381
|
-
</div>`).join('');
|
|
382
|
-
box.innerHTML = (files.length ? `<div>Всего доступно одновременных сессий: <b style="color:var(--text)">${lic.totalSeats}</b> (${lic.baseSeats} вечных + ${lic.addonSeats} временных)</div>` : '') + rows + resRows;
|
|
668
|
+
|
|
669
|
+
startFranchiseCountdowns();
|
|
383
670
|
}
|
|
384
671
|
|
|
385
672
|
async function refreshCabinetUser() {
|
|
@@ -394,14 +681,176 @@
|
|
|
394
681
|
catch { /* ignore */ }
|
|
395
682
|
}
|
|
396
683
|
|
|
397
|
-
function
|
|
398
|
-
|
|
684
|
+
async function createCabinetServer() {
|
|
685
|
+
if (!isUserReadyForOrders()) {
|
|
686
|
+
requireAuthAndOferta(() => createCabinetServer());
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const inp = $('#inpNewServerName');
|
|
690
|
+
const serverName = inp?.value.trim() || '';
|
|
691
|
+
try {
|
|
692
|
+
const res = await api('/api/cabinet/servers/create', {
|
|
693
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
694
|
+
serverName,
|
|
695
|
+
});
|
|
696
|
+
if (inp) inp.value = '';
|
|
697
|
+
if (res.user) {
|
|
698
|
+
currentUser = { ...res.user, sessionKey: currentUser.sessionKey };
|
|
699
|
+
onAuthenticated();
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
alert(err.message);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function renameCabinetServer(licenseId, currentName = '') {
|
|
708
|
+
const nextName = prompt('Введите новое название для сервера WORK:', currentName || '');
|
|
709
|
+
if (!nextName || nextName.trim() === currentName) return;
|
|
710
|
+
try {
|
|
711
|
+
const res = await api('/api/cabinet/servers/rename', {
|
|
712
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
713
|
+
licenseId,
|
|
714
|
+
serverName: nextName.trim(),
|
|
715
|
+
});
|
|
716
|
+
if (res.user) {
|
|
717
|
+
currentUser = { ...res.user, sessionKey: currentUser.sessionKey };
|
|
718
|
+
onAuthenticated();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (err) {
|
|
722
|
+
alert(err.message);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async function deleteCabinetServer(licenseId) {
|
|
727
|
+
if (!confirm('Удалить этот неактивированный сервер из вашего реестра?')) return;
|
|
728
|
+
try {
|
|
729
|
+
const res = await api('/api/cabinet/servers/delete', {
|
|
730
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
731
|
+
licenseId,
|
|
732
|
+
});
|
|
733
|
+
if (res.user) {
|
|
734
|
+
currentUser = { ...res.user, sessionKey: currentUser.sessionKey };
|
|
735
|
+
onAuthenticated();
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
alert(err.message);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function copyServerToken(licenseId) {
|
|
744
|
+
const inp = $(`#srvTok-${licenseId}`);
|
|
745
|
+
const val = inp?.value || '';
|
|
399
746
|
if (!val) return;
|
|
400
747
|
navigator.clipboard?.writeText(val);
|
|
401
|
-
const st = $(
|
|
748
|
+
const st = $(`#srvTokStatus-${licenseId}`);
|
|
402
749
|
if (st) {
|
|
403
|
-
st.textContent = 'Токен скопирован в буфер обмена!
|
|
404
|
-
setTimeout(() => { if (st.textContent.includes('скопирован')) st.textContent = ''; },
|
|
750
|
+
st.textContent = '✓ Токен сервера скопирован в буфер обмена!';
|
|
751
|
+
setTimeout(() => { if (st.textContent.includes('скопирован')) st.textContent = ''; }, 4000);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async function regenerateServerToken(licenseId) {
|
|
756
|
+
if (!currentUser?.sessionKey) return;
|
|
757
|
+
try {
|
|
758
|
+
const res = await api('/api/cabinet/token', {
|
|
759
|
+
sessionKey: currentUser.sessionKey,
|
|
760
|
+
licenseId,
|
|
761
|
+
});
|
|
762
|
+
if (res.user) {
|
|
763
|
+
currentUser = { ...res.user, sessionKey: currentUser.sessionKey };
|
|
764
|
+
onAuthenticated();
|
|
765
|
+
}
|
|
766
|
+
const st = $(`#srvTokStatus-${licenseId}`);
|
|
767
|
+
if (st) st.textContent = '✓ Выпущен новый токен для этого сервера.';
|
|
768
|
+
}
|
|
769
|
+
catch (err) {
|
|
770
|
+
alert(err.message);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function downloadServerLicFile(licenseId, fileIdx) {
|
|
775
|
+
const srv = getUserServersList().find(s => s.licenseId === licenseId);
|
|
776
|
+
const f = srv?.licenseFiles?.[Number(fileIdx)];
|
|
777
|
+
if (!f) return;
|
|
778
|
+
const json = JSON.stringify(f.licenseFile || f, null, 2);
|
|
779
|
+
const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
|
|
780
|
+
const url = URL.createObjectURL(blob);
|
|
781
|
+
const a = document.createElement('a');
|
|
782
|
+
a.href = url;
|
|
783
|
+
a.download = f.fileName || `${licenseId}.lic.json`;
|
|
784
|
+
document.body.appendChild(a);
|
|
785
|
+
a.click();
|
|
786
|
+
a.remove();
|
|
787
|
+
URL.revokeObjectURL(url);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function buyForSpecificServer(licenseId, variant = 'license_only') {
|
|
791
|
+
if (variant === 'addons_only') {
|
|
792
|
+
$$('#segPakVariant button').forEach(b => b.classList.toggle('on', b.dataset.variant === 'addons_only'));
|
|
793
|
+
syncClientSeats(5);
|
|
794
|
+
syncClientServerPicker(licenseId);
|
|
795
|
+
switchStage('clients');
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
const data = await api('/api/cabinet/orders/draft', {
|
|
800
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
801
|
+
licenseId,
|
|
802
|
+
includeBasePak: variant === 'pak',
|
|
803
|
+
includeBaseLicenseOnly: variant === 'license_only',
|
|
804
|
+
tier: 'enterprise',
|
|
805
|
+
addonSeats: 0,
|
|
806
|
+
years: 1,
|
|
807
|
+
});
|
|
808
|
+
if (data.user) {
|
|
809
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
810
|
+
onAuthenticated();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
alert(err.message);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
async function payFranchiseInstallment(reservationId, { monthNumber = null, earlyPayoff = false, paymentMethod = 'yookassa' } = {}) {
|
|
819
|
+
try {
|
|
820
|
+
const data = await api('/api/cabinet/franchises/pay-installment', {
|
|
821
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
822
|
+
reservationId,
|
|
823
|
+
monthNumber: monthNumber != null ? Number(monthNumber) : null,
|
|
824
|
+
earlyPayoff: Boolean(earlyPayoff),
|
|
825
|
+
paymentMethod,
|
|
826
|
+
});
|
|
827
|
+
if (data.user) {
|
|
828
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
829
|
+
onAuthenticated();
|
|
830
|
+
}
|
|
831
|
+
const url = data.invoiceUrl || data.confirmationUrl;
|
|
832
|
+
if (url) window.open(url, '_blank', 'noopener');
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
alert(err.message);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
async function cancelFranchiseByUser(reservationId) {
|
|
840
|
+
if (!confirm('Отменить резерв этой региональной франшизы?')) return;
|
|
841
|
+
try {
|
|
842
|
+
const data = await api('/api/cabinet/franchises/cancel', {
|
|
843
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
844
|
+
reservationId,
|
|
845
|
+
});
|
|
846
|
+
if (data.user) {
|
|
847
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
848
|
+
onAuthenticated();
|
|
849
|
+
}
|
|
850
|
+
refreshCatalogRegions();
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
alert(err.message);
|
|
405
854
|
}
|
|
406
855
|
}
|
|
407
856
|
|
|
@@ -493,8 +942,11 @@
|
|
|
493
942
|
const st = $('#pakOrderStatus');
|
|
494
943
|
if (st) st.textContent = 'Добавляем заказ в ваш Личный кабинет…';
|
|
495
944
|
try {
|
|
945
|
+
const pickedLicId = $('#selClientTargetServer')?.value || '';
|
|
496
946
|
const data = await api('/api/cabinet/orders/draft', {
|
|
497
947
|
sessionKey: currentUser?.sessionKey || '',
|
|
948
|
+
licenseId: pickedLicId,
|
|
949
|
+
createNewServer: pickedLicId === 'NEW',
|
|
498
950
|
includeBasePak,
|
|
499
951
|
includeBaseLicenseOnly,
|
|
500
952
|
tier,
|
|
@@ -1236,7 +1688,7 @@
|
|
|
1236
1688
|
/* ================= Привязка событий ================= */
|
|
1237
1689
|
function bind() {
|
|
1238
1690
|
document.addEventListener('click', (e) => {
|
|
1239
|
-
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');
|
|
1691
|
+
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-pay-order],[data-invoice-toggle],[data-cancel-order],[data-copy-server-token],[data-regen-server-token],[data-rename-server],[data-delete-server],[data-download-lic],[data-buy-for-server],[data-pay-installment],[data-cancel-franchise],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
|
|
1240
1692
|
if (!el) return;
|
|
1241
1693
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1242
1694
|
if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
|
|
@@ -1247,6 +1699,22 @@
|
|
|
1247
1699
|
return;
|
|
1248
1700
|
}
|
|
1249
1701
|
if (el.matches('[data-cancel-order]')) { e.stopPropagation(); cancelCabinetOrder(el.dataset.cancelOrder); return; }
|
|
1702
|
+
if (el.matches('[data-copy-server-token]')) { e.stopPropagation(); copyServerToken(el.dataset.copyServerToken); return; }
|
|
1703
|
+
if (el.matches('[data-regen-server-token]')) { e.stopPropagation(); regenerateServerToken(el.dataset.regenServerToken); return; }
|
|
1704
|
+
if (el.matches('[data-rename-server]')) { e.stopPropagation(); renameCabinetServer(el.dataset.renameServer, el.dataset.currentName || ''); return; }
|
|
1705
|
+
if (el.matches('[data-delete-server]')) { e.stopPropagation(); deleteCabinetServer(el.dataset.deleteServer); return; }
|
|
1706
|
+
if (el.matches('[data-download-lic]')) { e.stopPropagation(); downloadServerLicFile(el.dataset.downloadLic, el.dataset.fileIdx || 0); return; }
|
|
1707
|
+
if (el.matches('[data-buy-for-server]')) { e.stopPropagation(); buyForSpecificServer(el.dataset.buyForServer, el.dataset.buyVariant || 'license_only'); return; }
|
|
1708
|
+
if (el.matches('[data-pay-installment]')) {
|
|
1709
|
+
e.stopPropagation();
|
|
1710
|
+
payFranchiseInstallment(el.dataset.payInstallment, {
|
|
1711
|
+
monthNumber: el.dataset.month ? Number(el.dataset.month) : null,
|
|
1712
|
+
earlyPayoff: el.dataset.early === '1',
|
|
1713
|
+
paymentMethod: el.dataset.payMethod || 'yookassa',
|
|
1714
|
+
});
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (el.matches('[data-cancel-franchise]')) { e.stopPropagation(); cancelFranchiseByUser(el.dataset.cancelFranchise); return; }
|
|
1250
1718
|
if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
|
|
1251
1719
|
if (el.matches('th[data-sort]')) {
|
|
1252
1720
|
SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
|
|
@@ -1291,11 +1759,12 @@
|
|
|
1291
1759
|
$$('#segPakVariant button').forEach(b => b.classList.toggle('on', b.dataset.variant === 'addons_only'));
|
|
1292
1760
|
}
|
|
1293
1761
|
syncClientSeats(5);
|
|
1762
|
+
syncClientServerPicker();
|
|
1294
1763
|
buyClientConfig();
|
|
1295
1764
|
break;
|
|
1296
1765
|
case 'buy-client-config': buyClientConfig(); break;
|
|
1766
|
+
case 'create-server': createCabinetServer(); break;
|
|
1297
1767
|
case 'gen-hub-token': generateCabinetToken(); break;
|
|
1298
|
-
case 'copy-hub-token': copyCabinetToken(); break;
|
|
1299
1768
|
case 'refresh-cabinet': refreshCabinetUser(); break;
|
|
1300
1769
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
1301
1770
|
case 'cert-close': $('#certModal')?.classList.remove('open'); break;
|
|
@@ -1325,7 +1794,10 @@
|
|
|
1325
1794
|
$('#rngPartnerSales')?.addEventListener('input', recalcPartner);
|
|
1326
1795
|
$('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
|
|
1327
1796
|
bindSeg('segPakTier', recalcBaseOrder);
|
|
1328
|
-
bindSeg('segPakVariant', () =>
|
|
1797
|
+
bindSeg('segPakVariant', () => {
|
|
1798
|
+
syncClientSeats($('#inpPakExtraSeatsNum')?.value || 0);
|
|
1799
|
+
syncClientServerPicker();
|
|
1800
|
+
});
|
|
1329
1801
|
window.addEventListener('message', (e) => {
|
|
1330
1802
|
if (e.data?.type === 'odant:order-paid') {
|
|
1331
1803
|
refreshCabinetUser();
|