work-license-server 1.0.15 → 1.0.17
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 +57 -41
- package/assets/landing.css +150 -0
- package/assets/landing.js +195 -170
- package/package.json +1 -1
- package/src/admin.js +189 -186
- package/src/crypto.js +72 -0
- package/src/landing.js +40 -39
- package/src/server.js +20 -1
- package/src/store.js +56 -10
package/assets/landing.js
CHANGED
|
@@ -129,12 +129,47 @@
|
|
|
129
129
|
card.style.setProperty('--my', (e.clientY - r.top) + 'px');
|
|
130
130
|
}, { passive: true });
|
|
131
131
|
|
|
132
|
-
/* ================= Вход / Регистрация
|
|
132
|
+
/* ================= Вход / Регистрация и постоянная сессия в браузере ================= */
|
|
133
|
+
function readCookieSession() {
|
|
134
|
+
try {
|
|
135
|
+
const m = String(document.cookie || '').match(/(?:^|;\s*)odant_work_session=([^;]+)/);
|
|
136
|
+
return m?.[1] ? decodeURIComponent(m[1].trim()) : '';
|
|
137
|
+
}
|
|
138
|
+
catch { return ''; }
|
|
139
|
+
}
|
|
140
|
+
function persistSessionInBrowser(sessionKey = '', email = '', isAdmin = false) {
|
|
141
|
+
try {
|
|
142
|
+
if (sessionKey) {
|
|
143
|
+
localStorage.setItem('odant_work_session', sessionKey);
|
|
144
|
+
sessionStorage.setItem('odant_work_session', sessionKey);
|
|
145
|
+
if (isAdmin) {
|
|
146
|
+
localStorage.setItem('odant_work_admin_token', sessionKey);
|
|
147
|
+
sessionStorage.setItem('odant_work_admin_token', sessionKey);
|
|
148
|
+
}
|
|
149
|
+
document.cookie = `odant_work_session=${encodeURIComponent(sessionKey)}; path=/; max-age=31536000; SameSite=Lax`;
|
|
150
|
+
}
|
|
151
|
+
if (email) {
|
|
152
|
+
localStorage.setItem('odant_work_email', email);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { /* приватный режим */ }
|
|
156
|
+
}
|
|
157
|
+
function clearSessionFromBrowser() {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.removeItem('odant_work_session');
|
|
160
|
+
localStorage.removeItem('odant_work_admin_token');
|
|
161
|
+
sessionStorage.removeItem('odant_work_session');
|
|
162
|
+
sessionStorage.removeItem('odant_work_admin_token');
|
|
163
|
+
document.cookie = 'odant_work_session=; path=/; max-age=0; SameSite=Lax';
|
|
164
|
+
}
|
|
165
|
+
catch { /* ignore */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
133
168
|
function openAuth(hintMsg = '', intentTitle = '') {
|
|
134
169
|
if ($('#authErrorMsg')) $('#authErrorMsg').textContent = hintMsg || '';
|
|
135
170
|
const intentBox = $('#authIntentNotice');
|
|
136
171
|
if (intentBox) {
|
|
137
|
-
if (intentTitle) {
|
|
172
|
+
if (intentTitle && !currentUser) {
|
|
138
173
|
intentBox.style.display = '';
|
|
139
174
|
intentBox.innerHTML = `Выбранная позиция: <b>${esc(intentTitle)}</b>.<br>После входа она автоматически появится как заказ в вашем Личном кабинете.`;
|
|
140
175
|
}
|
|
@@ -143,9 +178,36 @@
|
|
|
143
178
|
intentBox.textContent = '';
|
|
144
179
|
}
|
|
145
180
|
}
|
|
146
|
-
if (
|
|
181
|
+
if (currentUser) {
|
|
182
|
+
if ($('#authTitle')) $('#authTitle').textContent = 'Ваш аккаунт';
|
|
183
|
+
if ($('#authSub')) $('#authSub').textContent = 'Сессия сохранена в браузере';
|
|
184
|
+
if ($('#authStepLoggedIn')) $('#authStepLoggedIn').style.display = '';
|
|
185
|
+
if ($('#authStepEmail')) $('#authStepEmail').style.display = 'none';
|
|
186
|
+
if ($('#authStepCode')) $('#authStepCode').style.display = 'none';
|
|
187
|
+
if ($('#authLoggedEmail')) $('#authLoggedEmail').textContent = currentUser.email || '';
|
|
188
|
+
if ($('#authLoggedRoleBadge')) {
|
|
189
|
+
$('#authLoggedRoleBadge').innerHTML = currentUser.isAdmin
|
|
190
|
+
? '<span class="tag ok">BOSS · Главный администратор</span>'
|
|
191
|
+
: '<span class="tag">Клиент ODANT-WORK</span>';
|
|
192
|
+
}
|
|
193
|
+
if ($('#btnGoAdminModal')) $('#btnGoAdminModal').style.display = currentUser.isAdmin ? '' : 'none';
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if ($('#authTitle')) $('#authTitle').textContent = 'Вход / Регистрация';
|
|
197
|
+
if ($('#authSub')) $('#authSub').textContent = 'На ваш Email придёт одноразовый 6-значный код подтверждения';
|
|
198
|
+
if ($('#authStepLoggedIn')) $('#authStepLoggedIn').style.display = 'none';
|
|
199
|
+
if ($('#authStepEmail')) $('#authStepEmail').style.display = '';
|
|
200
|
+
if ($('#authStepCode')) $('#authStepCode').style.display = 'none';
|
|
201
|
+
try {
|
|
202
|
+
const savedEmail = localStorage.getItem('odant_work_email') || '';
|
|
203
|
+
if (savedEmail && $('#authEmail') && !$('#authEmail').value) {
|
|
204
|
+
$('#authEmail').value = savedEmail;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* ignore */ }
|
|
208
|
+
setTimeout(() => $('#authEmail')?.focus(), 50);
|
|
209
|
+
}
|
|
147
210
|
$('#authModal')?.classList.add('open');
|
|
148
|
-
setTimeout(() => $('#authEmail')?.focus(), 50);
|
|
149
211
|
}
|
|
150
212
|
function closeAuth() {
|
|
151
213
|
$('#authModal')?.classList.remove('open');
|
|
@@ -224,7 +286,7 @@
|
|
|
224
286
|
try {
|
|
225
287
|
const data = await api('/api/auth/verify-code', { email, code });
|
|
226
288
|
currentUser = data;
|
|
227
|
-
|
|
289
|
+
persistSessionInBrowser(data.sessionKey, data.email, data.isAdmin);
|
|
228
290
|
resetAuthStep();
|
|
229
291
|
onAuthenticated();
|
|
230
292
|
closeAuth();
|
|
@@ -245,18 +307,14 @@
|
|
|
245
307
|
}
|
|
246
308
|
}
|
|
247
309
|
|
|
248
|
-
function performLogout() {
|
|
310
|
+
async function performLogout() {
|
|
311
|
+
try { await api('/api/auth/logout', {}); } catch { /* ignore */ }
|
|
249
312
|
currentUser = null;
|
|
250
313
|
pendingAuthAction = null;
|
|
251
|
-
|
|
252
|
-
localStorage.removeItem('odant_work_session');
|
|
253
|
-
localStorage.removeItem('odant_work_admin_token');
|
|
254
|
-
sessionStorage.removeItem('odant_work_admin_token');
|
|
255
|
-
} catch { /* ignore */ }
|
|
314
|
+
clearSessionFromBrowser();
|
|
256
315
|
if ($('#tileAddons')) $('#tileAddons').style.display = 'none';
|
|
257
316
|
if ($('#tileAdmin')) $('#tileAdmin').style.display = 'none';
|
|
258
317
|
if ($('#btnAuthText')) $('#btnAuthText').textContent = 'Войти / Регистрация';
|
|
259
|
-
if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = 'none';
|
|
260
318
|
closeAuth();
|
|
261
319
|
if (currentStage === 'addons') switchStage('product');
|
|
262
320
|
moveIndicator();
|
|
@@ -264,20 +322,36 @@
|
|
|
264
322
|
|
|
265
323
|
async function restoreSession() {
|
|
266
324
|
let key = '';
|
|
267
|
-
try {
|
|
325
|
+
try {
|
|
326
|
+
key = localStorage.getItem('odant_work_session')
|
|
327
|
+
|| sessionStorage.getItem('odant_work_session')
|
|
328
|
+
|| localStorage.getItem('odant_work_admin_token')
|
|
329
|
+
|| readCookieSession()
|
|
330
|
+
|| '';
|
|
331
|
+
}
|
|
332
|
+
catch { key = readCookieSession(); }
|
|
268
333
|
if (!key) return;
|
|
269
334
|
try {
|
|
270
335
|
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
|
|
271
336
|
if (data.authenticated && data.user) {
|
|
272
|
-
currentUser = { ...data.user, sessionKey: key };
|
|
337
|
+
currentUser = { ...data.user, sessionKey: data.user.sessionKey || key };
|
|
338
|
+
persistSessionInBrowser(currentUser.sessionKey, currentUser.email, currentUser.isAdmin);
|
|
273
339
|
onAuthenticated();
|
|
274
340
|
if (location.hash === '#addons' && isUserReadyForOrders()) switchStage('addons', { scroll: false });
|
|
275
341
|
}
|
|
276
342
|
}
|
|
277
|
-
catch { /* сессия
|
|
343
|
+
catch { /* сессия отозвана */ }
|
|
278
344
|
}
|
|
279
345
|
|
|
280
346
|
let franchiseCountdownTimer = null;
|
|
347
|
+
let activeCabinetTab = 'servers';
|
|
348
|
+
|
|
349
|
+
function switchCabinetTab(tab = 'servers') {
|
|
350
|
+
activeCabinetTab = tab === 'franchises' ? 'franchises' : 'servers';
|
|
351
|
+
$$('[data-cab-tab]').forEach(b => b.classList.toggle('active', b.dataset.cabTab === activeCabinetTab));
|
|
352
|
+
if ($('#cabSecServers')) $('#cabSecServers').style.display = activeCabinetTab === 'servers' ? '' : 'none';
|
|
353
|
+
if ($('#cabSecFranchises')) $('#cabSecFranchises').style.display = activeCabinetTab === 'franchises' ? '' : 'none';
|
|
354
|
+
}
|
|
281
355
|
|
|
282
356
|
function onAuthenticated() {
|
|
283
357
|
if (!currentUser) return;
|
|
@@ -360,11 +434,11 @@
|
|
|
360
434
|
const box = $('#cabinetServersRegistry');
|
|
361
435
|
if (!box) return;
|
|
362
436
|
const servers = getUserServersList();
|
|
363
|
-
if ($('#
|
|
437
|
+
if ($('#navCabServers')) $('#navCabServers').textContent = `Серверы и устройства (${servers.length})`;
|
|
364
438
|
|
|
365
439
|
if (!servers.length) {
|
|
366
|
-
box.innerHTML = `<div style="padding:
|
|
367
|
-
В
|
|
440
|
+
box.innerHTML = `<div style="padding:18px;color:var(--muted);text-align:center;">
|
|
441
|
+
В реестре пока нет серверов WORK. Введите название нового сервера справа вверху и нажмите <b>«+ Добавить сервер»</b>.
|
|
368
442
|
</div>`;
|
|
369
443
|
return;
|
|
370
444
|
}
|
|
@@ -375,12 +449,12 @@
|
|
|
375
449
|
const tok = s.hubToken || currentUser?.hubToken || '';
|
|
376
450
|
const hwidBound = Boolean(s.hwidBound || s.hardware?.hwidBound || s.hardware?.nodeFingerprint);
|
|
377
451
|
const hwidText = hwidBound
|
|
378
|
-
?
|
|
379
|
-
: '
|
|
452
|
+
? `HWID: <code>${esc(s.hardware?.hwid || '')}</code>`
|
|
453
|
+
: 'ожидает привязки токена в WORK → Лицензия';
|
|
380
454
|
|
|
381
455
|
const baseBadge = s.baseActivated
|
|
382
|
-
? `<span class="tag ok">✓
|
|
383
|
-
: `<span class="tag">Базовая не
|
|
456
|
+
? `<span class="tag ok">✓ Базовая: ${s.baseSeats || 10} вечн. мест + ИИ</span>`
|
|
457
|
+
: `<span class="tag">Базовая не активна (1 сессия BOSS)</span>`;
|
|
384
458
|
|
|
385
459
|
const files = s.licenseFiles || [];
|
|
386
460
|
const activeAddons = s.activeAddons || [];
|
|
@@ -390,118 +464,83 @@
|
|
|
390
464
|
const canDelete = !s.baseActivated && !activeAddons.length && !paidOrders.length;
|
|
391
465
|
|
|
392
466
|
const baseActionsHtml = !s.baseActivated
|
|
393
|
-
? `<
|
|
394
|
-
|
|
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>
|
|
396
|
-
</div>`
|
|
467
|
+
? `<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>
|
|
468
|
+
<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>`
|
|
397
469
|
: '';
|
|
398
470
|
|
|
399
|
-
const
|
|
400
|
-
?
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<span class="small muted">действует до ${esc(exp)}</span>
|
|
406
|
-
</div>`;
|
|
407
|
-
}).join('')}
|
|
408
|
-
</div>`
|
|
409
|
-
: `<div class="small muted" style="margin-top:6px">Дополнительные временные места не подключены.</div>`;
|
|
471
|
+
const addonsSummaryHtml = activeAddons.length
|
|
472
|
+
? activeAddons.map(sub => {
|
|
473
|
+
const exp = sub.expiresAt ? new Date(sub.expiresAt).toLocaleDateString('ru-RU') : '—';
|
|
474
|
+
return `<span class="tag ok" style="margin-right:6px">+${sub.seats} мест · ${esc(sub.tierName || sub.tier)} до ${esc(exp)}</span>`;
|
|
475
|
+
}).join('')
|
|
476
|
+
: `<span class="small muted">Доп. места не подключены</span>`;
|
|
410
477
|
|
|
411
478
|
const filesHtml = files.length
|
|
412
|
-
? `<div style="margin-top:
|
|
413
|
-
<
|
|
414
|
-
|
|
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('')}
|
|
424
|
-
</div>
|
|
479
|
+
? `<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
480
|
+
<span class="small muted">Файлы лицензий (<code>#system</code>):</span>
|
|
481
|
+
${files.map((f, fIdx) => `<button class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-download-lic="${esc(lid)}" data-file-idx="${fIdx}">↓ ${esc(f.fileName || 'license.lic.json')}</button>`).join('')}
|
|
425
482
|
</div>`
|
|
426
483
|
: '';
|
|
427
484
|
|
|
428
485
|
const pendingOrdersHtml = pendingOrders.length
|
|
429
|
-
? `<div style="margin-top:
|
|
430
|
-
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--warn);margin-bottom:8px">Ожидают оплаты по этому серверу (${pendingOrders.length})</div>
|
|
486
|
+
? `<div style="margin-top:10px;padding:10px 12px;border-radius:8px;border:1px solid var(--line-strong);background:color-mix(in srgb, var(--warn) 6%, transparent)">
|
|
431
487
|
${pendingOrders.map(o => {
|
|
432
488
|
const title = (o.items && o.items.length) ? o.items.map(it => esc(it.title)).join(' + ') : esc(o.tierName || o.orderId);
|
|
433
|
-
return `<div style="
|
|
434
|
-
<div
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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>
|
|
489
|
+
return `<div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
|
|
490
|
+
<div class="small"><b>Ожидает оплаты:</b> ${title} · <b>${rub(o.amountRub)}</b></div>
|
|
491
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
492
|
+
<button class="btn btn-primary btn-sm" style="padding:5px 10px;font-size:12px" data-pay-order="${esc(o.orderId)}" data-pay-method="yookassa">Оплатить ЮKassa</button>
|
|
493
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px" data-invoice-toggle="${esc(o.orderId)}">Счёт юрлицу</button>
|
|
494
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;color:var(--danger)" data-cancel-order="${esc(o.orderId)}">Отменить</button>
|
|
445
495
|
</div>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<button class="btn btn-primary btn-sm" style="margin-top:8px" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Сформировать и открыть счёт-договор →</button>
|
|
496
|
+
</div>
|
|
497
|
+
<div id="invBox-${esc(o.orderId)}" style="display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--line)">
|
|
498
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
499
|
+
<input type="text" id="invCompany-${esc(o.orderId)}" value="${esc(o.customer?.name || currentUser?.companyName || '')}" placeholder="Организация (ООО «Маслоторг»)" style="width:240px;padding:6px 10px;font-size:13px">
|
|
500
|
+
<input type="text" id="invInn-${esc(o.orderId)}" value="${esc(o.customer?.inn || currentUser?.inn || '')}" placeholder="ИНН" style="width:150px;padding:6px 10px;font-size:13px">
|
|
501
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12.5px" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Открыть счёт-договор →</button>
|
|
453
502
|
</div>
|
|
454
503
|
</div>`;
|
|
455
504
|
}).join('')}
|
|
456
505
|
</div>`
|
|
457
506
|
: '';
|
|
458
507
|
|
|
459
|
-
return `<div
|
|
460
|
-
<div style="display:flex;justify-content:space-between;align-items:
|
|
461
|
-
<div>
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
${baseBadge}
|
|
466
|
-
</div>
|
|
467
|
-
<div class="small muted" style="margin-top:6px">${hwidText}</div>
|
|
508
|
+
return `<div class="sec-item">
|
|
509
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
510
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
511
|
+
<b style="font-size:15px">${esc(name)}</b>
|
|
512
|
+
<span class="small muted">· <code>${esc(lid)}</code> · ${hwidText}</span>
|
|
513
|
+
${baseBadge}
|
|
468
514
|
</div>
|
|
469
|
-
<div style="display:flex;gap:
|
|
470
|
-
<span class="
|
|
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)}"
|
|
515
|
+
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
516
|
+
<span class="small muted" style="margin-right:4px">Сессий: <b>${s.totalSeats}</b></span>
|
|
517
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px" data-rename-server="${esc(lid)}" data-current-name="${esc(name)}">Переименовать</button>
|
|
518
|
+
${canDelete ? `<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;color:var(--danger)" data-delete-server="${esc(lid)}">Удалить</button>` : ''}
|
|
473
519
|
</div>
|
|
474
520
|
</div>
|
|
475
521
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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>
|
|
522
|
+
<!-- Строка токена привязки этого сервера -->
|
|
523
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
524
|
+
<span class="small muted" style="min-width:115px">Токен привязки:</span>
|
|
525
|
+
<input type="text" id="srvTok-${esc(lid)}" readonly value="${esc(tok)}" style="flex:1;min-width:200px;max-width:440px;padding:6px 10px;font-family:monospace;font-size:12px;border-radius:8px">
|
|
526
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12px" data-copy-server-token="${esc(lid)}">Скопировать токен</button>
|
|
527
|
+
<button class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px" data-regen-server-token="${esc(lid)}">Перевыпустить</button>
|
|
528
|
+
<span class="small" id="srvTokStatus-${esc(lid)}" style="color:var(--ok)"></span>
|
|
529
|
+
</div>
|
|
488
530
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
<b>Базовая лицензия:</b> ${s.baseActivated ? '<span style="color:var(--ok)">Активирована (10 вечных мест + ИИ-агенты)</span>' : '<span style="color:var(--muted)">Не активирована</span>'}
|
|
497
|
-
</div>
|
|
531
|
+
<!-- Строка лицензий и докупки мест -->
|
|
532
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">
|
|
533
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
534
|
+
<span class="small muted" style="min-width:115px">Доп. лицензии:</span>
|
|
535
|
+
${addonsSummaryHtml}
|
|
536
|
+
</div>
|
|
537
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
498
538
|
${baseActionsHtml}
|
|
499
|
-
<
|
|
500
|
-
${addonsListHtml}
|
|
501
|
-
${filesHtml}
|
|
539
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px" data-buy-for-server="${esc(lid)}" data-buy-variant="addons_only">+ Докупить рабочие места</button>
|
|
502
540
|
</div>
|
|
503
541
|
</div>
|
|
504
542
|
|
|
543
|
+
${filesHtml}
|
|
505
544
|
${pendingOrdersHtml}
|
|
506
545
|
</div>`;
|
|
507
546
|
}).join('');
|
|
@@ -544,11 +583,11 @@
|
|
|
544
583
|
if (!box) return;
|
|
545
584
|
const franchises = currentUser?.franchises || currentUser?.reservations || [];
|
|
546
585
|
const activeList = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
547
|
-
if ($('#
|
|
586
|
+
if ($('#navCabFranchises')) $('#navCabFranchises').textContent = `Региональные франшизы (${activeList.length})`;
|
|
548
587
|
|
|
549
588
|
if (!activeList.length) {
|
|
550
|
-
box.innerHTML = `<div style="padding:
|
|
551
|
-
У вас пока нет зарезервированных или действующих региональных франшиз. Нажмите <b
|
|
589
|
+
box.innerHTML = `<div style="padding:18px;color:var(--muted);text-align:center;">
|
|
590
|
+
У вас пока нет зарезервированных или действующих региональных франшиз. Нажмите <b>«+ Выбрать регион на карте РФ»</b> справа вверху.
|
|
552
591
|
</div>`;
|
|
553
592
|
startFranchiseCountdowns();
|
|
554
593
|
return;
|
|
@@ -569,29 +608,28 @@
|
|
|
569
608
|
const remSec = Math.max(0, Math.floor((deadline - now) / 1000));
|
|
570
609
|
|
|
571
610
|
const statusBadge = !f.depositPaid
|
|
572
|
-
? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)"
|
|
611
|
+
? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Оплата залога: <b data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</b></span>`
|
|
573
612
|
: (remainingRub <= 0
|
|
574
|
-
? `<span class="tag ok">✓
|
|
575
|
-
: `<span class="tag ok">✓ Залог оплачен ·
|
|
613
|
+
? `<span class="tag ok">✓ Погашена полностью (12/12)</span>`
|
|
614
|
+
: `<span class="tag ok">✓ Залог оплачен · График ${paidMonths}/12 мес.</span>`);
|
|
576
615
|
|
|
577
616
|
const depositAlertHtml = !f.depositPaid
|
|
578
|
-
? `<div style="margin-top:
|
|
579
|
-
<div>
|
|
580
|
-
<
|
|
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>
|
|
617
|
+
? `<div style="margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid rgba(245,158,11,.45);background:rgba(245,158,11,.08);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
618
|
+
<div class="small">
|
|
619
|
+
<b>Оплатите начальный взнос (${rub(depositRub)}) через ЮKassa в течение <span style="font-family:monospace;font-size:13.5px" data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</span></b>, иначе резерв автоматически отменится.
|
|
582
620
|
</div>
|
|
583
|
-
<div style="display:flex;gap:
|
|
584
|
-
<button class="btn btn-primary btn-sm" data-pay-order="${esc(f.orderId)}" data-pay-method="yookassa">Оплатить
|
|
585
|
-
<button class="btn btn-ghost btn-sm" data-cancel-franchise="${esc(rid)}"
|
|
621
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
622
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12px" data-pay-order="${esc(f.orderId)}" data-pay-method="yookassa">Оплатить ${rub(depositRub)} (ЮKassa)</button>
|
|
623
|
+
<button class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px;color:var(--danger)" data-cancel-franchise="${esc(rid)}">Отменить резерв</button>
|
|
586
624
|
</div>
|
|
587
625
|
</div>`
|
|
588
626
|
: '';
|
|
589
627
|
|
|
590
628
|
const nextInst = f.nextInstallment || schedule.find(x => x.status !== 'paid');
|
|
591
629
|
const scheduleControlsHtml = (f.depositPaid && remainingRub > 0)
|
|
592
|
-
? `<div style="display:flex;gap:
|
|
593
|
-
${nextInst ? `<button class="btn btn-primary btn-sm" data-pay-installment="${esc(rid)}" data-month="${nextInst.monthNumber}">Оплатить
|
|
594
|
-
<button class="btn btn-ghost btn-sm" data-pay-installment="${esc(rid)}" data-early="1"
|
|
630
|
+
? `<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
|
|
631
|
+
${nextInst ? `<button class="btn btn-primary btn-sm" style="padding:5px 10px;font-size:12px" data-pay-installment="${esc(rid)}" data-month="${nextInst.monthNumber}">Оплатить месяц №${nextInst.monthNumber} (${rub(nextInst.amountRub)})</button>` : ''}
|
|
632
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px" data-pay-installment="${esc(rid)}" data-early="1">Погасить досрочно (${rub(remainingRub)})</button>
|
|
595
633
|
</div>`
|
|
596
634
|
: '';
|
|
597
635
|
|
|
@@ -607,62 +645,48 @@
|
|
|
607
645
|
stHtml = '<span class="tag bad">Просрочен</span>';
|
|
608
646
|
}
|
|
609
647
|
else if (inst.displayStatus === 'next') {
|
|
610
|
-
stHtml = '<span class="tag" style="color:var(--brand-2)"
|
|
648
|
+
stHtml = '<span class="tag" style="color:var(--brand-2)">К оплате</span>';
|
|
611
649
|
}
|
|
612
650
|
|
|
613
651
|
const actHtml = (!isPaid && f.depositPaid)
|
|
614
652
|
? `<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"
|
|
653
|
+
<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
654
|
<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
655
|
</div>`
|
|
618
|
-
: (isPaid ? `<span class="small muted">Закрыт</span>` : `<span class="small muted">После
|
|
656
|
+
: (isPaid ? `<span class="small muted">Закрыт</span>` : `<span class="small muted">После залога</span>`);
|
|
619
657
|
|
|
620
658
|
return `<tr>
|
|
621
659
|
<td><b>Месяц ${inst.monthNumber} / 12</b></td>
|
|
622
660
|
<td>${esc(dueStr)}</td>
|
|
623
|
-
<td
|
|
661
|
+
<td><b>${rub(inst.amountRub)}</b></td>
|
|
624
662
|
<td>${stHtml}</td>
|
|
625
663
|
<td style="text-align:right">${actHtml}</td>
|
|
626
664
|
</tr>`;
|
|
627
665
|
}).join('');
|
|
628
666
|
|
|
629
|
-
return `<div
|
|
630
|
-
<div style="display:flex;justify-content:space-between;align-items:
|
|
631
|
-
<div>
|
|
632
|
-
<
|
|
633
|
-
|
|
634
|
-
|
|
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>
|
|
667
|
+
return `<div class="sec-item">
|
|
668
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
669
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
670
|
+
<b style="font-size:15px">${esc(f.regionName)}</b>
|
|
671
|
+
<span class="small muted">· ${esc(f.district || f.regionCode)} · <code>${esc(rid)}</code></span>
|
|
672
|
+
${statusBadge}
|
|
641
673
|
</div>
|
|
642
674
|
${scheduleControlsHtml}
|
|
643
675
|
</div>
|
|
644
|
-
|
|
645
|
-
|
|
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>
|
|
676
|
+
<div class="small muted" style="margin-top:4px">
|
|
677
|
+
Паушальный взнос: <b>${rub(totalFeeRub)}</b> · Залог: <b>${rub(depositRub)}</b> · Оплачено: <b>${rub(totalPaidRub)}</b> (${pct}%) · Остаток: <b>${rub(remainingRub)}</b>
|
|
665
678
|
</div>
|
|
679
|
+
${depositAlertHtml}
|
|
680
|
+
<table class="sec-table" style="margin-top:10px">
|
|
681
|
+
<thead><tr>
|
|
682
|
+
<th>Период</th>
|
|
683
|
+
<th>Дата платежа</th>
|
|
684
|
+
<th>Сумма</th>
|
|
685
|
+
<th>Статус</th>
|
|
686
|
+
<th style="text-align:right">Оплата</th>
|
|
687
|
+
</tr></thead>
|
|
688
|
+
<tbody>${scheduleRowsHtml}</tbody>
|
|
689
|
+
</table>
|
|
666
690
|
</div>`;
|
|
667
691
|
}).join('');
|
|
668
692
|
|
|
@@ -958,6 +982,7 @@
|
|
|
958
982
|
onAuthenticated();
|
|
959
983
|
}
|
|
960
984
|
if (st) st.textContent = '';
|
|
985
|
+
switchCabinetTab('servers');
|
|
961
986
|
switchStage('addons');
|
|
962
987
|
}
|
|
963
988
|
catch (err) {
|
|
@@ -1428,6 +1453,7 @@
|
|
|
1428
1453
|
}
|
|
1429
1454
|
if (msg) msg.textContent = '';
|
|
1430
1455
|
refreshCatalogRegions();
|
|
1456
|
+
switchCabinetTab('franchises');
|
|
1431
1457
|
switchStage('addons');
|
|
1432
1458
|
}
|
|
1433
1459
|
catch (err) {
|
|
@@ -1688,8 +1714,9 @@
|
|
|
1688
1714
|
/* ================= Привязка событий ================= */
|
|
1689
1715
|
function bind() {
|
|
1690
1716
|
document.addEventListener('click', (e) => {
|
|
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');
|
|
1717
|
+
const el = e.target.closest('[data-stage],[data-cab-tab],[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');
|
|
1692
1718
|
if (!el) return;
|
|
1719
|
+
if (el.matches('[data-cab-tab]')) { e.preventDefault(); switchCabinetTab(el.dataset.cabTab); return; }
|
|
1693
1720
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1694
1721
|
if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
|
|
1695
1722
|
if (el.matches('[data-invoice-toggle]')) {
|
|
@@ -1727,14 +1754,12 @@
|
|
|
1727
1754
|
if (el.dataset.stage && el !== document.body) { e.preventDefault(); switchStage(el.dataset.stage); return; }
|
|
1728
1755
|
switch (el.dataset.action) {
|
|
1729
1756
|
case 'auth':
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
openAuth();
|
|
1737
|
-
}
|
|
1757
|
+
pendingAuthAction = null;
|
|
1758
|
+
openAuth();
|
|
1759
|
+
break;
|
|
1760
|
+
case 'go-cabinet':
|
|
1761
|
+
closeAuth();
|
|
1762
|
+
switchStage('addons');
|
|
1738
1763
|
break;
|
|
1739
1764
|
case 'auth-close': closeAuth(); break;
|
|
1740
1765
|
case 'send-code': sendAuthCode(); break;
|
package/package.json
CHANGED