work-license-server 1.0.6 → 1.0.7
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 +100 -15
- package/package.json +1 -1
- package/src/landing.js +48 -17
- package/src/store.js +1 -0
package/assets/landing.js
CHANGED
|
@@ -320,12 +320,67 @@
|
|
|
320
320
|
$('#botText').value = '/bind ' + currentUser.license.support.bindCode;
|
|
321
321
|
}
|
|
322
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
|
+
}
|
|
329
|
+
}
|
|
330
|
+
renderCabinetLicenses();
|
|
323
331
|
updateOfertaBanners();
|
|
324
332
|
updateClientSupportCard();
|
|
325
333
|
moveIndicator();
|
|
326
334
|
recalcAddons();
|
|
327
335
|
}
|
|
328
336
|
|
|
337
|
+
function renderCabinetLicenses() {
|
|
338
|
+
const box = $('#cabinetLicensesBox');
|
|
339
|
+
if (!box) return;
|
|
340
|
+
const lic = currentUser?.license;
|
|
341
|
+
const files = lic?.licenseFiles || [];
|
|
342
|
+
if (!files.length) {
|
|
343
|
+
box.innerHTML = `<div style="padding:12px 14px;border-radius:10px;border:1px dashed var(--border);color:var(--muted);">
|
|
344
|
+
У вас пока нет приобретённых лицензий. Оформите базовую постоянную лицензию (или комплект с машиной) в Шаге 3 ниже — после оплаты файлы лицензий сразу появятся здесь и будут доступны для автоматической загрузки по вашему токену.
|
|
345
|
+
</div>`;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const rows = files.map(f => {
|
|
349
|
+
const lf = f.licenseFile || f;
|
|
350
|
+
const isBase = lf.kind === 'base' || lf.perpetual;
|
|
351
|
+
const expText = isBase ? 'Бессрочно (постоянная)' : (lf.expiresAt ? 'до ' + new Date(lf.expiresAt).toLocaleDateString('ru-RU') + ` (${lf.years || 1} г.)` : '—');
|
|
352
|
+
const title = isBase ? 'Первичная постоянная лицензия (10 вечных мест + ИИ-агенты)' : `Временная лицензия (+${lf.seats} конк. мест · ${String(lf.tier || '').toUpperCase()})`;
|
|
353
|
+
return `<div style="display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);align-items:center;">
|
|
354
|
+
<div><b>${esc(title)}</b><br><code style="font-size:11.5px;color:var(--muted)">${esc(f.fileName || lf.fileId)}</code></div>
|
|
355
|
+
<div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">${esc(expText)}</span></div>
|
|
356
|
+
</div>`;
|
|
357
|
+
}).join('');
|
|
358
|
+
box.innerHTML = `<div>Всего доступно одновременных сессий: <b style="color:var(--text)">${lic.totalSeats}</b> (${lic.baseSeats} вечных + ${lic.addonSeats} временных)</div>` + rows;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function refreshCabinetUser() {
|
|
362
|
+
if (!currentUser?.sessionKey) return;
|
|
363
|
+
try {
|
|
364
|
+
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(currentUser.sessionKey));
|
|
365
|
+
if (data.authenticated && data.user) {
|
|
366
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
367
|
+
onAuthenticated();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch { /* ignore */ }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function copyCabinetToken() {
|
|
374
|
+
const val = $('#inpCabinetHubToken')?.value || '';
|
|
375
|
+
if (!val) return;
|
|
376
|
+
navigator.clipboard?.writeText(val);
|
|
377
|
+
const st = $('#tokenCopyStatus');
|
|
378
|
+
if (st) {
|
|
379
|
+
st.textContent = 'Токен скопирован в буфер обмена! Вставьте его в локальном WORK → Лицензия.';
|
|
380
|
+
setTimeout(() => { if (st.textContent.includes('скопирован')) st.textContent = ''; }, 5000);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
329
384
|
/* ================= Калькуляторы ================= */
|
|
330
385
|
function volumeDiscount(seats) {
|
|
331
386
|
const s = Math.max(0, Math.floor(Number(seats) || 0));
|
|
@@ -346,28 +401,43 @@
|
|
|
346
401
|
const lines = (rows) => rows.map(([k, v]) => `<div><span>${k}</span><b>${v}</b></div>`).join('');
|
|
347
402
|
|
|
348
403
|
function recalcAddons() {
|
|
349
|
-
const
|
|
404
|
+
const baseMode = $('#segCabinetBase button.on')?.dataset.base || 'license_only';
|
|
405
|
+
const includeBasePak = baseMode === 'pak';
|
|
406
|
+
const includeBaseLicenseOnly = baseMode === 'license_only';
|
|
407
|
+
const basePrice = includeBasePak
|
|
408
|
+
? (D.product?.basePriceRub || 5_000_000)
|
|
409
|
+
: (includeBaseLicenseOnly ? (D.product?.baseLicensePriceRub || 4_000_000) : 0);
|
|
410
|
+
|
|
411
|
+
const minSeats = (includeBasePak || includeBaseLicenseOnly) ? 0 : 1;
|
|
412
|
+
const seats = Math.max(minSeats, Number($('#inpSeats').value) || 0);
|
|
350
413
|
const years = Math.min(5, Math.max(1, Number($('#inpYears').value) || 1));
|
|
351
414
|
const tier = tierById(segValue('segTier'));
|
|
352
415
|
const disc = volumeDiscount(seats);
|
|
353
416
|
const gross = seats * tier.pricePerUserYearRub * years;
|
|
354
417
|
const saved = Math.round(gross * disc.discountPercent / 100);
|
|
355
|
-
|
|
356
|
-
|
|
418
|
+
const addonsNet = gross - saved;
|
|
419
|
+
const total = basePrice + addonsNet;
|
|
420
|
+
|
|
421
|
+
$('#lblTotal').textContent = rub(total);
|
|
422
|
+
$('#lblDiscountBadge').textContent = seats > 0 && disc.discountPercent
|
|
357
423
|
? `Скидка за количество −${disc.discountPercent}% · экономия ${rub(saved)}`
|
|
358
|
-
: 'Скидка от количества начинается с 5 мест';
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
['
|
|
365
|
-
['
|
|
366
|
-
|
|
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
|
+
}
|
|
433
|
+
breakdownRows.push(['Итого мест после покупки', `${(D.product?.baseSeats || 10) + (currentUser?.license?.addonSeats || 0) + seats}`]);
|
|
434
|
+
$('#lblBreakdown').innerHTML = lines(breakdownRows);
|
|
367
435
|
markLadder('ladderAddons', seats);
|
|
368
436
|
}
|
|
369
437
|
function syncSeats(v) {
|
|
370
|
-
const
|
|
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));
|
|
371
441
|
$('#inpSeats').value = n;
|
|
372
442
|
$('#rngAddonSeats').value = Math.min(150, n);
|
|
373
443
|
recalcAddons();
|
|
@@ -430,15 +500,22 @@
|
|
|
430
500
|
requireAuthAndOferta(() => submitAddonOrder(paymentMethod));
|
|
431
501
|
return;
|
|
432
502
|
}
|
|
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);
|
|
508
|
+
|
|
433
509
|
const st = $('#orderStatus');
|
|
434
510
|
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
435
511
|
try {
|
|
436
512
|
const data = await api('/api/orders/checkout', {
|
|
437
513
|
sessionKey: currentUser?.sessionKey || '',
|
|
438
|
-
includeBasePak
|
|
514
|
+
includeBasePak,
|
|
515
|
+
includeBaseLicenseOnly,
|
|
439
516
|
paymentMethod,
|
|
440
517
|
tier: segValue('segTier'),
|
|
441
|
-
addonSeats
|
|
518
|
+
addonSeats,
|
|
442
519
|
years: Number($('#inpYears').value) || 1,
|
|
443
520
|
licenseId: $('#inpLicId').value.trim(),
|
|
444
521
|
customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
|
|
@@ -1165,6 +1242,8 @@
|
|
|
1165
1242
|
case 'order-addons': submitAddonOrder('yookassa'); break;
|
|
1166
1243
|
case 'order-addons-invoice': submitAddonOrder('invoice'); break;
|
|
1167
1244
|
case 'gen-hub-token': generateCabinetToken(); break;
|
|
1245
|
+
case 'copy-hub-token': copyCabinetToken(); break;
|
|
1246
|
+
case 'refresh-cabinet': refreshCabinetUser(); break;
|
|
1168
1247
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
1169
1248
|
case 'bot-send': sendBot(); break;
|
|
1170
1249
|
case 'cert-close': $('#certModal')?.classList.remove('open'); break;
|
|
@@ -1191,8 +1270,14 @@
|
|
|
1191
1270
|
$('#rngPartnerSales')?.addEventListener('input', recalcPartner);
|
|
1192
1271
|
$('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
|
|
1193
1272
|
bindSeg('segTier', recalcAddons);
|
|
1273
|
+
bindSeg('segCabinetBase', () => { syncSeats($('#inpSeats')?.value || 0); });
|
|
1194
1274
|
bindSeg('segPakTier', recalcBaseOrder);
|
|
1195
1275
|
bindSeg('segPakVariant', recalcBaseOrder);
|
|
1276
|
+
window.addEventListener('message', (e) => {
|
|
1277
|
+
if (e.data?.type === 'odant:order-paid') {
|
|
1278
|
+
refreshCabinetUser();
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
1196
1281
|
window.addEventListener('resize', moveIndicator);
|
|
1197
1282
|
window.addEventListener('hashchange', () => { const id = location.hash.slice(1); if (id && id !== currentStage) switchStage(id, { scroll: false }); });
|
|
1198
1283
|
}
|
package/package.json
CHANGED
package/src/landing.js
CHANGED
|
@@ -184,7 +184,7 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
184
184
|
<button class="tab" data-stage="agents">ИИ-агенты</button>
|
|
185
185
|
<button class="tab" data-stage="pricing">Тарифы</button>
|
|
186
186
|
<button class="tab" data-stage="order">Комплекс</button>
|
|
187
|
-
<button class="tab" id="tileAddons" data-stage="addons" style="display:none;"
|
|
187
|
+
<button class="tab" id="tileAddons" data-stage="addons" style="display:none;">Личный кабинет</button>
|
|
188
188
|
<button class="tab" data-stage="franchise">Франшиза</button>
|
|
189
189
|
<button class="tab" data-stage="support">Поддержка</button>
|
|
190
190
|
</nav>
|
|
@@ -403,37 +403,63 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
403
403
|
<!-- ============ ДОКУПКА ЛИЦЕНЗИЙ И ЛИЧНЫЙ КАБИНЕТ (только после входа и подписания оферты) ============ -->
|
|
404
404
|
<section class="stage" id="stage-addons">
|
|
405
405
|
<div class="sec-head">
|
|
406
|
-
<div><span class="kicker">Личный кабинет владельца</span><h2 class="h2">Токен
|
|
406
|
+
<div><span class="kicker">Личный кабинет владельца</span><h2 class="h2">Токен привязки, ваши лицензии и покупка</h2></div>
|
|
407
407
|
<p id="userBadgeBox"></p>
|
|
408
408
|
</div>
|
|
409
409
|
<div class="card oferta-banner" data-oferta-banner></div>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
<
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
<
|
|
416
|
-
<
|
|
410
|
+
|
|
411
|
+
<div class="split" style="margin-bottom:20px">
|
|
412
|
+
<div class="card">
|
|
413
|
+
<span class="kicker">Шаг 1 · Привязка вашего локального сервера WORK</span>
|
|
414
|
+
<h3 class="h3" style="margin:4px 0 8px">Токен владельца (содержит URL сервера поддержки)</h3>
|
|
415
|
+
<p class="small" style="margin:0 0 12px">Скопируйте этот токен и вставьте его в разделе <b>WORK → Лицензия</b> вашей локальной системы. По этому токену система автоматически скачивает и обновляет ваши лицензии в папку <code>#system</code>.</p>
|
|
416
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
|
|
417
|
+
<input type="text" id="inpCabinetHubToken" readonly placeholder="owk1..." style="flex:1;min-width:220px;font-family:monospace">
|
|
418
|
+
<button class="btn btn-primary btn-sm" data-action="copy-hub-token">Скопировать токен</button>
|
|
419
|
+
<button class="btn btn-ghost btn-sm" data-action="gen-hub-token">Перевыпустить</button>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="small" id="tokenCopyStatus" style="color:var(--ok);min-height:18px;margin-top:6px"></div>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div class="card">
|
|
425
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap">
|
|
426
|
+
<div>
|
|
427
|
+
<span class="kicker">Шаг 2 · Ваши выпущенные лицензии</span>
|
|
428
|
+
<h3 class="h3" style="margin:4px 0 6px">Файлы лицензий в личном кабинете</h3>
|
|
429
|
+
</div>
|
|
430
|
+
<button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить</button>
|
|
431
|
+
</div>
|
|
432
|
+
<div id="cabinetLicensesBox" class="small" style="margin-top:8px;line-height:1.55"></div>
|
|
417
433
|
</div>
|
|
418
434
|
</div>
|
|
435
|
+
|
|
419
436
|
<div class="split">
|
|
420
437
|
<div class="card">
|
|
438
|
+
<span class="kicker">Шаг 3 · Приобретение базовой и дополнительных лицензий</span>
|
|
439
|
+
<div class="field" style="margin-top:8px">
|
|
440
|
+
<label>Базовая постоянная лицензия (10 вечных пользователей + включение ИИ-агентов)</label>
|
|
441
|
+
<div class="seg" id="segCabinetBase">
|
|
442
|
+
<button data-base="license_only" class="on">Базовая лицензия (без железа)<small>${rub(P.baseLicensePriceRub || 4_000_000)} · 10 вечных мест</small></button>
|
|
443
|
+
<button data-base="pak">Комплект с машиной<small>${rub(P.basePriceRub)} · сервер + 10 мест</small></button>
|
|
444
|
+
<button data-base="none">Только доп. временные<small>0 ₽ · базовая уже куплена</small></button>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
421
447
|
<div class="field">
|
|
422
|
-
<label
|
|
448
|
+
<label>Дополнительные временные места (сверх 10 вечных)</label>
|
|
423
449
|
<div style="display:flex;align-items:center;gap:16px">
|
|
424
|
-
<input type="range" id="rngAddonSeats" min="
|
|
425
|
-
<input type="number" id="inpSeats" min="
|
|
450
|
+
<input type="range" id="rngAddonSeats" min="0" max="150" value="0" style="flex:1">
|
|
451
|
+
<input type="number" id="inpSeats" min="0" max="2000" value="0" style="width:110px">
|
|
426
452
|
</div>
|
|
427
453
|
</div>
|
|
428
454
|
<div class="field">
|
|
429
|
-
<label>Уровень
|
|
455
|
+
<label>Уровень поддержки дополнительных мест</label>
|
|
430
456
|
<div class="seg" id="segTier">${Object.values(SLA_TIERS).map(t => `<button data-tier="${t.id}" class="${t.id === 'business' ? 'on' : ''}">${esc(t.name)}<small>${rub(t.pricePerUserYearRub)} · ${esc(t.reactionLabel)}</small></button>`).join('')}</div>
|
|
431
457
|
</div>
|
|
432
458
|
<div class="row2">
|
|
433
|
-
<div class="field"><label for="inpYears"
|
|
434
|
-
<div class="field"><label for="inpLicId"
|
|
459
|
+
<div class="field"><label for="inpYears">Срок временных лицензий, лет (кратно годам)</label><input type="number" id="inpYears" min="1" max="5" value="1"></div>
|
|
460
|
+
<div class="field"><label for="inpLicId">Идентификатор лицензии</label><input type="text" id="inpLicId" readonly></div>
|
|
435
461
|
</div>
|
|
436
|
-
<div class="field" style="margin-top:6px"><label>Скидка от
|
|
462
|
+
<div class="field" style="margin-top:6px"><label>Скидка от количества доп. мест</label>${renderLadder('ladderAddons')}</div>
|
|
437
463
|
</div>
|
|
438
464
|
<div class="card summary card-glow">
|
|
439
465
|
<span class="kicker">К оплате</span>
|
|
@@ -809,7 +835,12 @@ export function renderSandboxPaymentPage(order) {
|
|
|
809
835
|
});
|
|
810
836
|
const data = await res.json();
|
|
811
837
|
document.getElementById('st').textContent = data.status || event;
|
|
812
|
-
document.getElementById('msg').
|
|
838
|
+
document.getElementById('msg').innerHTML = data.status === 'paid'
|
|
839
|
+
? 'Оплата успешно проведена! Файлы лицензий выпущены в вашем Личном кабинете.<br><button class="btn cancel" style="margin-top:10px" onclick="if(window.opener){window.opener.postMessage({type:\\'odant:order-paid\\'},\\'*\\');window.close();}else{location.href=\\'/#addons\\';}">Вернуться в Личный кабинет →</button>'
|
|
840
|
+
: 'Платёж отменён.';
|
|
841
|
+
if (data.status === 'paid' && window.opener) {
|
|
842
|
+
try { window.opener.postMessage({ type: 'odant:order-paid', orderId: ORDER.orderId }, '*'); } catch {}
|
|
843
|
+
}
|
|
813
844
|
}
|
|
814
845
|
</script>
|
|
815
846
|
</body>
|
package/src/store.js
CHANGED