work-license-server 1.0.15 → 1.0.16
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 +21 -32
- package/assets/landing.css +150 -0
- package/assets/landing.js +108 -147
- package/package.json +1 -1
- package/src/admin.js +165 -186
- package/src/landing.js +26 -38
package/assets/admin.js
CHANGED
|
@@ -89,10 +89,20 @@
|
|
|
89
89
|
setTimeout(() => $('#admLoginEmail')?.focus(), 50);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
function moveIndicator() {
|
|
93
|
+
const active = $('#tabs .tab.active');
|
|
94
|
+
const ind = $('#tabInd');
|
|
95
|
+
if (!ind) return;
|
|
96
|
+
if (!active) { ind.style.width = '0px'; return; }
|
|
97
|
+
ind.style.left = active.offsetLeft + 'px';
|
|
98
|
+
ind.style.width = active.offsetWidth + 'px';
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
function showAdminApp(login = 'BOSS') {
|
|
93
102
|
$('#adminLoginScreen').style.display = 'none';
|
|
94
103
|
$('#adminAppShell').style.display = '';
|
|
95
|
-
if ($('#admUserLabel')) $('#admUserLabel').textContent =
|
|
104
|
+
if ($('#admUserLabel')) $('#admUserLabel').textContent = login;
|
|
105
|
+
requestAnimationFrame(moveIndicator);
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
async function sendAdminOtpCode() {
|
|
@@ -201,7 +211,7 @@
|
|
|
201
211
|
/* ================= Вкладки и форматирование ================= */
|
|
202
212
|
function setTab(tab) {
|
|
203
213
|
activeTab = tab || 'servers';
|
|
204
|
-
$$('
|
|
214
|
+
$$('[data-adm-tab]').forEach(b => b.classList.toggle('active', b.dataset.admTab === activeTab));
|
|
205
215
|
$$('[data-adm-sec]').forEach(sec => {
|
|
206
216
|
sec.style.display = sec.dataset.admSec === activeTab ? '' : 'none';
|
|
207
217
|
});
|
|
@@ -265,34 +275,13 @@
|
|
|
265
275
|
const users = data.users || [];
|
|
266
276
|
|
|
267
277
|
const activeFranchises = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
268
|
-
const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
|
|
269
|
-
const paidOrders = orders.filter(o => o.status === 'paid');
|
|
270
|
-
const pendingOrders = orders.filter(o => o.status === 'pending' || o.status === 'awaiting_invoice');
|
|
271
|
-
const paidRub = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
272
|
-
const pendingRub = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
273
278
|
const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
|
|
274
279
|
|
|
275
|
-
if ($('#
|
|
276
|
-
if ($('#
|
|
277
|
-
if ($('#
|
|
278
|
-
if ($('#
|
|
279
|
-
if ($('#
|
|
280
|
-
|
|
281
|
-
if ($('#admStatLic')) {
|
|
282
|
-
$('#admStatLic').textContent = String(licenses.length);
|
|
283
|
-
$('#admStatLicSub').textContent = `серверов · ${totalSeats} мест`;
|
|
284
|
-
$('#admStatFranchises').textContent = String(activeFranchises.length);
|
|
285
|
-
const overdueCnt = activeFranchises.filter(f => (f.overdueMonthsCount || 0) > 0).length;
|
|
286
|
-
$('#admStatFranchisesSub').textContent = overdueCnt > 0 ? `просрочек: ${overdueCnt}` : 'активных франшиз';
|
|
287
|
-
$('#admStatPaid').textContent = rub(paidRub);
|
|
288
|
-
$('#admStatPaidSub').textContent = `оплачено заказов: ${paidOrders.length}`;
|
|
289
|
-
$('#admStatPending').textContent = rub(pendingRub);
|
|
290
|
-
$('#admStatPendingSub').textContent = `ожидают оплаты: ${pendingOrders.length}`;
|
|
291
|
-
$('#admStatUsers').textContent = String(users.length);
|
|
292
|
-
$('#admStatUsersSub').textContent = `админов BOSS: ${users.filter(u => u.isAdmin).length}`;
|
|
293
|
-
$('#admStatTickets').textContent = String(openTickets.length);
|
|
294
|
-
$('#admStatTicketsSub').textContent = `открытых из ${tickets.length}`;
|
|
295
|
-
}
|
|
280
|
+
if ($('#navAdmServers')) $('#navAdmServers').textContent = `Серверы и лицензии (${licenses.length})`;
|
|
281
|
+
if ($('#navAdmFranchises')) $('#navAdmFranchises').textContent = `Франшизы (${activeFranchises.length})`;
|
|
282
|
+
if ($('#navAdmOrders')) $('#navAdmOrders').textContent = `Заказы и счета (${orders.length})`;
|
|
283
|
+
if ($('#navAdmUsers')) $('#navAdmUsers').textContent = `Клиенты и роли (${users.length})`;
|
|
284
|
+
if ($('#navAdmTickets')) $('#navAdmTickets').textContent = `Тикеты поддержки (${openTickets.length})`;
|
|
296
285
|
|
|
297
286
|
renderServersTable(licenses);
|
|
298
287
|
renderFranchisesTable(franchises);
|
|
@@ -364,10 +353,8 @@
|
|
|
364
353
|
<button class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-open-grant="${esc(lid)}" data-srv-name="${esc(srvName)}">+ Начислить доп. места</button>
|
|
365
354
|
</div>
|
|
366
355
|
</td>
|
|
367
|
-
<td>
|
|
368
|
-
<
|
|
369
|
-
<button class="btn btn-primary btn-sm" data-cert="${esc(lid)}">Файлы .lic.json (${(l.licenseFiles || []).length})</button>
|
|
370
|
-
</div>
|
|
356
|
+
<td style="text-align:right">
|
|
357
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 10px;font-size:12px" data-cert="${esc(lid)}">.lic.json (${(l.licenseFiles || []).length})</button>
|
|
371
358
|
</td>
|
|
372
359
|
</tr>`;
|
|
373
360
|
}).join('') || '<tr><td colspan="6" class="muted">Серверов по заданному фильтру не найдено</td></tr>';
|
|
@@ -949,12 +936,14 @@
|
|
|
949
936
|
$$('.modal.open').forEach(m => m.classList.remove('open'));
|
|
950
937
|
}
|
|
951
938
|
});
|
|
939
|
+
window.addEventListener('resize', moveIndicator);
|
|
952
940
|
}
|
|
953
941
|
|
|
954
942
|
function start() {
|
|
955
943
|
applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
|
|
956
944
|
bind();
|
|
957
945
|
setTab('servers');
|
|
946
|
+
if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
|
|
958
947
|
checkAdminSession();
|
|
959
948
|
}
|
|
960
949
|
|
package/assets/landing.css
CHANGED
|
@@ -614,6 +614,156 @@ body[data-stage="admin"] .ai-fab { display: none; }
|
|
|
614
614
|
.admin-left { position: static; }
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
+
/* ---------- Двухколоночная навигация по разделам (эталон Скрина 1: Личный кабинет и Админ-панель) ---------- */
|
|
618
|
+
.sec-layout {
|
|
619
|
+
display: grid;
|
|
620
|
+
grid-template-columns: 230px minmax(0, 1fr);
|
|
621
|
+
gap: 28px;
|
|
622
|
+
align-items: start;
|
|
623
|
+
margin-top: 4px;
|
|
624
|
+
}
|
|
625
|
+
@media (max-width: 920px) {
|
|
626
|
+
.sec-layout { grid-template-columns: 1fr; gap: 16px; }
|
|
627
|
+
}
|
|
628
|
+
.sec-nav {
|
|
629
|
+
display: flex;
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 4px;
|
|
632
|
+
position: sticky;
|
|
633
|
+
top: 84px;
|
|
634
|
+
}
|
|
635
|
+
.sec-nav-item {
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
justify-content: space-between;
|
|
639
|
+
width: 100%;
|
|
640
|
+
padding: 9px 14px;
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
border: 1.5px solid transparent;
|
|
643
|
+
background: transparent;
|
|
644
|
+
color: var(--text-2);
|
|
645
|
+
font-size: 14px;
|
|
646
|
+
font-weight: 500;
|
|
647
|
+
text-align: left;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
transition: background .15s ease, border-color .15s ease, color .15s ease;
|
|
650
|
+
}
|
|
651
|
+
.sec-nav-item:hover {
|
|
652
|
+
background: color-mix(in srgb, var(--brand) 6%, transparent);
|
|
653
|
+
color: var(--text);
|
|
654
|
+
}
|
|
655
|
+
.sec-nav-item.active {
|
|
656
|
+
border-color: #3b82f6;
|
|
657
|
+
background: rgba(59, 130, 246, 0.08);
|
|
658
|
+
color: #1d4ed8;
|
|
659
|
+
font-weight: 600;
|
|
660
|
+
}
|
|
661
|
+
html[data-theme="dark"] .sec-nav-item.active {
|
|
662
|
+
border-color: #3b82f6;
|
|
663
|
+
background: rgba(59, 130, 246, 0.16);
|
|
664
|
+
color: #60a5fa;
|
|
665
|
+
}
|
|
666
|
+
.sec-nav-sep {
|
|
667
|
+
height: 1px;
|
|
668
|
+
background: var(--line);
|
|
669
|
+
margin: 6px 4px;
|
|
670
|
+
}
|
|
671
|
+
.sec-main {
|
|
672
|
+
min-width: 0;
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-direction: column;
|
|
675
|
+
gap: 12px;
|
|
676
|
+
}
|
|
677
|
+
.sec-bar {
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
justify-content: space-between;
|
|
681
|
+
gap: 12px;
|
|
682
|
+
flex-wrap: wrap;
|
|
683
|
+
min-height: 36px;
|
|
684
|
+
}
|
|
685
|
+
.sec-title {
|
|
686
|
+
margin: 0;
|
|
687
|
+
font-size: 16px;
|
|
688
|
+
font-weight: 700;
|
|
689
|
+
color: var(--text);
|
|
690
|
+
}
|
|
691
|
+
.sec-controls {
|
|
692
|
+
display: flex;
|
|
693
|
+
align-items: center;
|
|
694
|
+
gap: 8px;
|
|
695
|
+
flex-wrap: wrap;
|
|
696
|
+
}
|
|
697
|
+
.sec-controls input[type="text"],
|
|
698
|
+
.sec-controls input[type="search"],
|
|
699
|
+
.sec-controls select {
|
|
700
|
+
width: auto !important;
|
|
701
|
+
min-width: 0;
|
|
702
|
+
height: 34px;
|
|
703
|
+
padding: 6px 12px;
|
|
704
|
+
font-size: 13px;
|
|
705
|
+
border-radius: 8px;
|
|
706
|
+
border: 1px solid var(--line-strong);
|
|
707
|
+
background: var(--panel-solid);
|
|
708
|
+
color: var(--text);
|
|
709
|
+
}
|
|
710
|
+
.sec-controls input[type="search"],
|
|
711
|
+
.sec-controls input[type="text"] {
|
|
712
|
+
width: 230px !important;
|
|
713
|
+
}
|
|
714
|
+
.sec-controls select {
|
|
715
|
+
width: 185px !important;
|
|
716
|
+
}
|
|
717
|
+
.sec-controls .btn {
|
|
718
|
+
height: 34px;
|
|
719
|
+
padding: 0 13px;
|
|
720
|
+
font-size: 13px;
|
|
721
|
+
border-radius: 8px;
|
|
722
|
+
}
|
|
723
|
+
.sec-box {
|
|
724
|
+
background: var(--panel-solid);
|
|
725
|
+
border: 1px solid var(--line-strong);
|
|
726
|
+
border-radius: 12px;
|
|
727
|
+
padding: 16px 20px;
|
|
728
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
|
729
|
+
overflow: hidden;
|
|
730
|
+
}
|
|
731
|
+
.sec-table {
|
|
732
|
+
width: 100%;
|
|
733
|
+
border-collapse: collapse;
|
|
734
|
+
font-size: 13.5px;
|
|
735
|
+
}
|
|
736
|
+
.sec-table th,
|
|
737
|
+
.sec-table td {
|
|
738
|
+
padding: 10px 10px;
|
|
739
|
+
text-align: left;
|
|
740
|
+
border-bottom: 1px solid var(--line);
|
|
741
|
+
vertical-align: middle;
|
|
742
|
+
white-space: normal;
|
|
743
|
+
word-break: break-word;
|
|
744
|
+
}
|
|
745
|
+
.sec-table th {
|
|
746
|
+
color: var(--muted);
|
|
747
|
+
font-size: 11px;
|
|
748
|
+
font-weight: 700;
|
|
749
|
+
letter-spacing: .05em;
|
|
750
|
+
text-transform: uppercase;
|
|
751
|
+
padding-top: 4px;
|
|
752
|
+
padding-bottom: 8px;
|
|
753
|
+
}
|
|
754
|
+
.sec-table tbody tr:last-child td {
|
|
755
|
+
border-bottom: 0;
|
|
756
|
+
}
|
|
757
|
+
.sec-table tbody tr:hover {
|
|
758
|
+
background: color-mix(in srgb, var(--brand) 4%, transparent);
|
|
759
|
+
}
|
|
760
|
+
.sec-item {
|
|
761
|
+
padding: 14px 0;
|
|
762
|
+
border-bottom: 1px solid var(--line);
|
|
763
|
+
}
|
|
764
|
+
.sec-item:first-child { padding-top: 2px; }
|
|
765
|
+
.sec-item:last-child { border-bottom: 0; padding-bottom: 2px; }
|
|
766
|
+
|
|
617
767
|
/* ---------- Оферта и вход администратора ---------- */
|
|
618
768
|
.oferta-banner {
|
|
619
769
|
padding: 16px 22px;
|
package/assets/landing.js
CHANGED
|
@@ -278,6 +278,14 @@
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
let franchiseCountdownTimer = null;
|
|
281
|
+
let activeCabinetTab = 'servers';
|
|
282
|
+
|
|
283
|
+
function switchCabinetTab(tab = 'servers') {
|
|
284
|
+
activeCabinetTab = tab === 'franchises' ? 'franchises' : 'servers';
|
|
285
|
+
$$('[data-cab-tab]').forEach(b => b.classList.toggle('active', b.dataset.cabTab === activeCabinetTab));
|
|
286
|
+
if ($('#cabSecServers')) $('#cabSecServers').style.display = activeCabinetTab === 'servers' ? '' : 'none';
|
|
287
|
+
if ($('#cabSecFranchises')) $('#cabSecFranchises').style.display = activeCabinetTab === 'franchises' ? '' : 'none';
|
|
288
|
+
}
|
|
281
289
|
|
|
282
290
|
function onAuthenticated() {
|
|
283
291
|
if (!currentUser) return;
|
|
@@ -360,11 +368,11 @@
|
|
|
360
368
|
const box = $('#cabinetServersRegistry');
|
|
361
369
|
if (!box) return;
|
|
362
370
|
const servers = getUserServersList();
|
|
363
|
-
if ($('#
|
|
371
|
+
if ($('#navCabServers')) $('#navCabServers').textContent = `Серверы и устройства (${servers.length})`;
|
|
364
372
|
|
|
365
373
|
if (!servers.length) {
|
|
366
|
-
box.innerHTML = `<div style="padding:
|
|
367
|
-
В
|
|
374
|
+
box.innerHTML = `<div style="padding:18px;color:var(--muted);text-align:center;">
|
|
375
|
+
В реестре пока нет серверов WORK. Введите название нового сервера справа вверху и нажмите <b>«+ Добавить сервер»</b>.
|
|
368
376
|
</div>`;
|
|
369
377
|
return;
|
|
370
378
|
}
|
|
@@ -375,12 +383,12 @@
|
|
|
375
383
|
const tok = s.hubToken || currentUser?.hubToken || '';
|
|
376
384
|
const hwidBound = Boolean(s.hwidBound || s.hardware?.hwidBound || s.hardware?.nodeFingerprint);
|
|
377
385
|
const hwidText = hwidBound
|
|
378
|
-
?
|
|
379
|
-
: '
|
|
386
|
+
? `HWID: <code>${esc(s.hardware?.hwid || '')}</code>`
|
|
387
|
+
: 'ожидает привязки токена в WORK → Лицензия';
|
|
380
388
|
|
|
381
389
|
const baseBadge = s.baseActivated
|
|
382
|
-
? `<span class="tag ok">✓
|
|
383
|
-
: `<span class="tag">Базовая не
|
|
390
|
+
? `<span class="tag ok">✓ Базовая: ${s.baseSeats || 10} вечн. мест + ИИ</span>`
|
|
391
|
+
: `<span class="tag">Базовая не активна (1 сессия BOSS)</span>`;
|
|
384
392
|
|
|
385
393
|
const files = s.licenseFiles || [];
|
|
386
394
|
const activeAddons = s.activeAddons || [];
|
|
@@ -390,118 +398,83 @@
|
|
|
390
398
|
const canDelete = !s.baseActivated && !activeAddons.length && !paidOrders.length;
|
|
391
399
|
|
|
392
400
|
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>`
|
|
401
|
+
? `<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>
|
|
402
|
+
<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
403
|
: '';
|
|
398
404
|
|
|
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>`;
|
|
405
|
+
const addonsSummaryHtml = activeAddons.length
|
|
406
|
+
? activeAddons.map(sub => {
|
|
407
|
+
const exp = sub.expiresAt ? new Date(sub.expiresAt).toLocaleDateString('ru-RU') : '—';
|
|
408
|
+
return `<span class="tag ok" style="margin-right:6px">+${sub.seats} мест · ${esc(sub.tierName || sub.tier)} до ${esc(exp)}</span>`;
|
|
409
|
+
}).join('')
|
|
410
|
+
: `<span class="small muted">Доп. места не подключены</span>`;
|
|
410
411
|
|
|
411
412
|
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>
|
|
413
|
+
? `<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
414
|
+
<span class="small muted">Файлы лицензий (<code>#system</code>):</span>
|
|
415
|
+
${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
416
|
</div>`
|
|
426
417
|
: '';
|
|
427
418
|
|
|
428
419
|
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>
|
|
420
|
+
? `<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
421
|
${pendingOrders.map(o => {
|
|
432
422
|
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>
|
|
423
|
+
return `<div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
|
|
424
|
+
<div class="small"><b>Ожидает оплаты:</b> ${title} · <b>${rub(o.amountRub)}</b></div>
|
|
425
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
426
|
+
<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>
|
|
427
|
+
<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px" data-invoice-toggle="${esc(o.orderId)}">Счёт юрлицу</button>
|
|
428
|
+
<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
429
|
</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>
|
|
430
|
+
</div>
|
|
431
|
+
<div id="invBox-${esc(o.orderId)}" style="display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--line)">
|
|
432
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
433
|
+
<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">
|
|
434
|
+
<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">
|
|
435
|
+
<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
436
|
</div>
|
|
454
437
|
</div>`;
|
|
455
438
|
}).join('')}
|
|
456
439
|
</div>`
|
|
457
440
|
: '';
|
|
458
441
|
|
|
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>
|
|
442
|
+
return `<div class="sec-item">
|
|
443
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
444
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
445
|
+
<b style="font-size:15px">${esc(name)}</b>
|
|
446
|
+
<span class="small muted">· <code>${esc(lid)}</code> · ${hwidText}</span>
|
|
447
|
+
${baseBadge}
|
|
468
448
|
</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)}"
|
|
449
|
+
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
450
|
+
<span class="small muted" style="margin-right:4px">Сессий: <b>${s.totalSeats}</b></span>
|
|
451
|
+
<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>
|
|
452
|
+
${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
453
|
</div>
|
|
474
454
|
</div>
|
|
475
455
|
|
|
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>
|
|
456
|
+
<!-- Строка токена привязки этого сервера -->
|
|
457
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
458
|
+
<span class="small muted" style="min-width:115px">Токен привязки:</span>
|
|
459
|
+
<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">
|
|
460
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12px" data-copy-server-token="${esc(lid)}">Скопировать токен</button>
|
|
461
|
+
<button class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px" data-regen-server-token="${esc(lid)}">Перевыпустить</button>
|
|
462
|
+
<span class="small" id="srvTokStatus-${esc(lid)}" style="color:var(--ok)"></span>
|
|
463
|
+
</div>
|
|
488
464
|
|
|
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>
|
|
465
|
+
<!-- Строка лицензий и докупки мест -->
|
|
466
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px">
|
|
467
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
468
|
+
<span class="small muted" style="min-width:115px">Доп. лицензии:</span>
|
|
469
|
+
${addonsSummaryHtml}
|
|
470
|
+
</div>
|
|
471
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
498
472
|
${baseActionsHtml}
|
|
499
|
-
<
|
|
500
|
-
${addonsListHtml}
|
|
501
|
-
${filesHtml}
|
|
473
|
+
<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
474
|
</div>
|
|
503
475
|
</div>
|
|
504
476
|
|
|
477
|
+
${filesHtml}
|
|
505
478
|
${pendingOrdersHtml}
|
|
506
479
|
</div>`;
|
|
507
480
|
}).join('');
|
|
@@ -544,11 +517,11 @@
|
|
|
544
517
|
if (!box) return;
|
|
545
518
|
const franchises = currentUser?.franchises || currentUser?.reservations || [];
|
|
546
519
|
const activeList = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
547
|
-
if ($('#
|
|
520
|
+
if ($('#navCabFranchises')) $('#navCabFranchises').textContent = `Региональные франшизы (${activeList.length})`;
|
|
548
521
|
|
|
549
522
|
if (!activeList.length) {
|
|
550
|
-
box.innerHTML = `<div style="padding:
|
|
551
|
-
У вас пока нет зарезервированных или действующих региональных франшиз. Нажмите <b
|
|
523
|
+
box.innerHTML = `<div style="padding:18px;color:var(--muted);text-align:center;">
|
|
524
|
+
У вас пока нет зарезервированных или действующих региональных франшиз. Нажмите <b>«+ Выбрать регион на карте РФ»</b> справа вверху.
|
|
552
525
|
</div>`;
|
|
553
526
|
startFranchiseCountdowns();
|
|
554
527
|
return;
|
|
@@ -569,29 +542,28 @@
|
|
|
569
542
|
const remSec = Math.max(0, Math.floor((deadline - now) / 1000));
|
|
570
543
|
|
|
571
544
|
const statusBadge = !f.depositPaid
|
|
572
|
-
? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)"
|
|
545
|
+
? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Оплата залога: <b data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</b></span>`
|
|
573
546
|
: (remainingRub <= 0
|
|
574
|
-
? `<span class="tag ok">✓
|
|
575
|
-
: `<span class="tag ok">✓ Залог оплачен ·
|
|
547
|
+
? `<span class="tag ok">✓ Погашена полностью (12/12)</span>`
|
|
548
|
+
: `<span class="tag ok">✓ Залог оплачен · График ${paidMonths}/12 мес.</span>`);
|
|
576
549
|
|
|
577
550
|
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>
|
|
551
|
+
? `<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">
|
|
552
|
+
<div class="small">
|
|
553
|
+
<b>Оплатите начальный взнос (${rub(depositRub)}) через ЮKassa в течение <span style="font-family:monospace;font-size:13.5px" data-deposit-deadline="${deadline}">${fmtCountdown(remSec)}</span></b>, иначе резерв автоматически отменится.
|
|
582
554
|
</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)}"
|
|
555
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
556
|
+
<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>
|
|
557
|
+
<button class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px;color:var(--danger)" data-cancel-franchise="${esc(rid)}">Отменить резерв</button>
|
|
586
558
|
</div>
|
|
587
559
|
</div>`
|
|
588
560
|
: '';
|
|
589
561
|
|
|
590
562
|
const nextInst = f.nextInstallment || schedule.find(x => x.status !== 'paid');
|
|
591
563
|
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"
|
|
564
|
+
? `<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
|
|
565
|
+
${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>` : ''}
|
|
566
|
+
<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
567
|
</div>`
|
|
596
568
|
: '';
|
|
597
569
|
|
|
@@ -607,62 +579,48 @@
|
|
|
607
579
|
stHtml = '<span class="tag bad">Просрочен</span>';
|
|
608
580
|
}
|
|
609
581
|
else if (inst.displayStatus === 'next') {
|
|
610
|
-
stHtml = '<span class="tag" style="color:var(--brand-2)"
|
|
582
|
+
stHtml = '<span class="tag" style="color:var(--brand-2)">К оплате</span>';
|
|
611
583
|
}
|
|
612
584
|
|
|
613
585
|
const actHtml = (!isPaid && f.depositPaid)
|
|
614
586
|
? `<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"
|
|
587
|
+
<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
588
|
<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
589
|
</div>`
|
|
618
|
-
: (isPaid ? `<span class="small muted">Закрыт</span>` : `<span class="small muted">После
|
|
590
|
+
: (isPaid ? `<span class="small muted">Закрыт</span>` : `<span class="small muted">После залога</span>`);
|
|
619
591
|
|
|
620
592
|
return `<tr>
|
|
621
593
|
<td><b>Месяц ${inst.monthNumber} / 12</b></td>
|
|
622
594
|
<td>${esc(dueStr)}</td>
|
|
623
|
-
<td
|
|
595
|
+
<td><b>${rub(inst.amountRub)}</b></td>
|
|
624
596
|
<td>${stHtml}</td>
|
|
625
597
|
<td style="text-align:right">${actHtml}</td>
|
|
626
598
|
</tr>`;
|
|
627
599
|
}).join('');
|
|
628
600
|
|
|
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>
|
|
601
|
+
return `<div class="sec-item">
|
|
602
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
603
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
604
|
+
<b style="font-size:15px">${esc(f.regionName)}</b>
|
|
605
|
+
<span class="small muted">· ${esc(f.district || f.regionCode)} · <code>${esc(rid)}</code></span>
|
|
606
|
+
${statusBadge}
|
|
641
607
|
</div>
|
|
642
608
|
${scheduleControlsHtml}
|
|
643
609
|
</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>
|
|
610
|
+
<div class="small muted" style="margin-top:4px">
|
|
611
|
+
Паушальный взнос: <b>${rub(totalFeeRub)}</b> · Залог: <b>${rub(depositRub)}</b> · Оплачено: <b>${rub(totalPaidRub)}</b> (${pct}%) · Остаток: <b>${rub(remainingRub)}</b>
|
|
665
612
|
</div>
|
|
613
|
+
${depositAlertHtml}
|
|
614
|
+
<table class="sec-table" style="margin-top:10px">
|
|
615
|
+
<thead><tr>
|
|
616
|
+
<th>Период</th>
|
|
617
|
+
<th>Дата платежа</th>
|
|
618
|
+
<th>Сумма</th>
|
|
619
|
+
<th>Статус</th>
|
|
620
|
+
<th style="text-align:right">Оплата</th>
|
|
621
|
+
</tr></thead>
|
|
622
|
+
<tbody>${scheduleRowsHtml}</tbody>
|
|
623
|
+
</table>
|
|
666
624
|
</div>`;
|
|
667
625
|
}).join('');
|
|
668
626
|
|
|
@@ -958,6 +916,7 @@
|
|
|
958
916
|
onAuthenticated();
|
|
959
917
|
}
|
|
960
918
|
if (st) st.textContent = '';
|
|
919
|
+
switchCabinetTab('servers');
|
|
961
920
|
switchStage('addons');
|
|
962
921
|
}
|
|
963
922
|
catch (err) {
|
|
@@ -1428,6 +1387,7 @@
|
|
|
1428
1387
|
}
|
|
1429
1388
|
if (msg) msg.textContent = '';
|
|
1430
1389
|
refreshCatalogRegions();
|
|
1390
|
+
switchCabinetTab('franchises');
|
|
1431
1391
|
switchStage('addons');
|
|
1432
1392
|
}
|
|
1433
1393
|
catch (err) {
|
|
@@ -1688,8 +1648,9 @@
|
|
|
1688
1648
|
/* ================= Привязка событий ================= */
|
|
1689
1649
|
function bind() {
|
|
1690
1650
|
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');
|
|
1651
|
+
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
1652
|
if (!el) return;
|
|
1653
|
+
if (el.matches('[data-cab-tab]')) { e.preventDefault(); switchCabinetTab(el.dataset.cabTab); return; }
|
|
1693
1654
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1694
1655
|
if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
|
|
1695
1656
|
if (el.matches('[data-invoice-toggle]')) {
|
package/package.json
CHANGED
package/src/admin.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Админ-панель центра лицензирования и франшиз ODANT-WORK (/admin).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Разделы:
|
|
8
|
-
* 1. Реестр серверов (устройств) и лицензий клиентов
|
|
9
|
-
* 2. Реестр региональных франшиз и 12-месячных графиков погашения
|
|
10
|
-
* 3. Заказы ЮKassa и безналичные счета-договоры
|
|
11
|
-
* 4. Зарегистрированные клиенты, оферты (ПЭП) и роли BOSS
|
|
12
|
-
* 5. Тикеты техподдержки SLA (MAX · Telegram · VK)
|
|
4
|
+
* Единая шапка с основным сайтом (О платформе · Клиентам · Франшиза · Личный кабинет · Админ-панель)
|
|
5
|
+
* и двухколоночная навигация со счётчиками в скобках (по эталону Скрина 1).
|
|
13
6
|
*/
|
|
14
7
|
import {
|
|
15
8
|
PAK_PRODUCT,
|
|
@@ -44,7 +37,7 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
44
37
|
<head>
|
|
45
38
|
<meta charset="utf-8">
|
|
46
39
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
47
|
-
<title>ODANT-WORK ·
|
|
40
|
+
<title>ODANT-WORK · Админ-панель</title>
|
|
48
41
|
<meta name="robots" content="noindex, nofollow">
|
|
49
42
|
<meta name="theme-color" content="#04060b">
|
|
50
43
|
<link rel="icon" type="image/png" href="/favicon.ico">
|
|
@@ -97,210 +90,196 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
97
90
|
<div style="margin-top:8px;text-align:center">
|
|
98
91
|
<a href="/" class="small muted">← Вернуться на главную страницу work.odant.org</a>
|
|
99
92
|
</div>
|
|
100
|
-
<div class="small muted" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line)">
|
|
101
|
-
Ключ ЦС Ed25519: <code>${esc(publicKey.slice(0, 26))}…</code>
|
|
102
|
-
</div>
|
|
103
93
|
</div>
|
|
104
94
|
</div>
|
|
105
95
|
|
|
106
|
-
<!-- Основная
|
|
96
|
+
<!-- Основная оболочка с единой шапкой сайта и двухколоночным меню разделов -->
|
|
107
97
|
<div class="shell" id="adminAppShell" style="display:none">
|
|
108
98
|
<header class="topbar">
|
|
109
|
-
<a href="/" class="brand" style="text-decoration:none">
|
|
99
|
+
<a href="/" class="brand" style="text-decoration:none" aria-label="ODANT-WORK — на главную">
|
|
110
100
|
<img src="/assets/odant-logo.png" alt="ODANT">
|
|
111
101
|
<span>
|
|
112
|
-
<span class="brand-name">ODANT<b>·</b>WORK
|
|
113
|
-
<span class="brand-sub"
|
|
102
|
+
<span class="brand-name">ODANT<b>·</b>WORK</span><br>
|
|
103
|
+
<span class="brand-sub">учёт · документооборот · процессы · приватный ИИ</span>
|
|
114
104
|
</span>
|
|
115
105
|
</a>
|
|
116
106
|
|
|
117
|
-
<nav class="tabs" id="
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
<
|
|
107
|
+
<nav class="tabs" id="tabs" aria-label="Разделы">
|
|
108
|
+
<span class="tab-ind" id="tabInd"></span>
|
|
109
|
+
<a class="tab" href="/#product" style="text-decoration:none">О платформе</a>
|
|
110
|
+
<a class="tab" href="/#clients" style="text-decoration:none">Клиентам</a>
|
|
111
|
+
<a class="tab" href="/#franchise" style="text-decoration:none">Франшиза</a>
|
|
112
|
+
<a class="tab" href="/#addons" style="text-decoration:none">Личный кабинет</a>
|
|
113
|
+
<a class="tab active" href="/admin" id="tileAdmin" style="text-decoration:none">Админ-панель</a>
|
|
123
114
|
</nav>
|
|
124
115
|
|
|
125
116
|
<div class="top-actions">
|
|
126
|
-
<a href="/#addons" class="btn btn-ghost btn-sm" style="text-decoration:none">← Личный кабинет</a>
|
|
127
117
|
<button class="icon-btn" id="btnTheme" title="Светлая / тёмная тема" aria-label="Переключить тему">${icon('sun')}</button>
|
|
128
|
-
<button class="btn btn-
|
|
129
|
-
<button class="btn btn-ghost btn-sm" data-action="admin-logout" title="Завершить сессию">${icon('lock')}<span id="admUserLabel">Выйти</span></button>
|
|
118
|
+
<button class="btn btn-primary btn-sm" data-action="admin-logout" title="Нажмите, чтобы выйти из сессии">${icon('user')}<span id="admUserLabel">BOSS</span></button>
|
|
130
119
|
</div>
|
|
131
120
|
</header>
|
|
132
121
|
|
|
133
|
-
<section class="stage active" style="padding-top:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
<section class="stage active" style="padding-top:24px">
|
|
123
|
+
<div class="sec-layout">
|
|
124
|
+
<!-- Левое вертикальное меню разделов со счётчиками в скобках (как на Скрине 1) -->
|
|
125
|
+
<aside class="sec-nav" aria-label="Разделы администрирования">
|
|
126
|
+
<button type="button" class="sec-nav-item active" data-adm-tab="servers" id="navAdmServers">Серверы и лицензии (0)</button>
|
|
127
|
+
<button type="button" class="sec-nav-item" data-adm-tab="franchises" id="navAdmFranchises">Франшизы (0)</button>
|
|
128
|
+
<div class="sec-nav-sep"></div>
|
|
129
|
+
<button type="button" class="sec-nav-item" data-adm-tab="orders" id="navAdmOrders">Заказы и счета (0)</button>
|
|
130
|
+
<button type="button" class="sec-nav-item" data-adm-tab="users" id="navAdmUsers">Клиенты и роли (0)</button>
|
|
131
|
+
<button type="button" class="sec-nav-item" data-adm-tab="tickets" id="navAdmTickets">Тикеты поддержки (0)</button>
|
|
132
|
+
</aside>
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<div>
|
|
148
|
-
<
|
|
149
|
-
|
|
134
|
+
<!-- Правая область выбранного раздела -->
|
|
135
|
+
<div class="sec-main">
|
|
136
|
+
<!-- 1. СЕРВЕРЫ И ЛИЦЕНЗИИ -->
|
|
137
|
+
<div data-adm-sec="servers">
|
|
138
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
139
|
+
<h2 class="sec-title">Серверы (устройства) WORK и лицензии клиентов</h2>
|
|
140
|
+
<div class="sec-controls">
|
|
141
|
+
<input type="search" id="inpFilterServers" placeholder="Поиск по клиенту, Email, HWID…">
|
|
142
|
+
<select id="selFilterServers">
|
|
143
|
+
<option value="all">Все серверы</option>
|
|
144
|
+
<option value="base_active">Базовая активна</option>
|
|
145
|
+
<option value="base_inactive">Без базовой лицензии</option>
|
|
146
|
+
<option value="hwid_bound">HWID привязан</option>
|
|
147
|
+
</select>
|
|
148
|
+
<button type="button" class="btn btn-primary btn-sm" data-action="open-create-server-modal">+ Добавить сервер</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="sec-box">
|
|
152
|
+
<table class="sec-table">
|
|
153
|
+
<thead><tr>
|
|
154
|
+
<th>Сервер / ID</th>
|
|
155
|
+
<th>Владелец / Организация</th>
|
|
156
|
+
<th>Железо (HWID)</th>
|
|
157
|
+
<th>Базовая лицензия</th>
|
|
158
|
+
<th>Доп. места и SLA</th>
|
|
159
|
+
<th style="text-align:right">Файлы</th>
|
|
160
|
+
</tr></thead>
|
|
161
|
+
<tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
150
164
|
</div>
|
|
151
|
-
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
152
|
-
<input type="search" id="inpFilterServers" placeholder="Поиск по клиенту, Email, ИНН, серверу, HWID…" style="width:280px;padding:8px 12px;font-size:13px">
|
|
153
|
-
<select id="selFilterServers" style="padding:8px 12px;font-size:13px">
|
|
154
|
-
<option value="all">Все серверы</option>
|
|
155
|
-
<option value="base_active">Базовая активирована</option>
|
|
156
|
-
<option value="base_inactive">Без базовой лицензии</option>
|
|
157
|
-
<option value="hwid_bound">Железо (HWID) привязано</option>
|
|
158
|
-
</select>
|
|
159
|
-
<button class="btn btn-primary btn-sm" data-action="open-create-server-modal">${icon('plus')} Добавить сервер / лицензию</button>
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="table-wrap">
|
|
163
|
-
<table class="t">
|
|
164
|
-
<thead><tr>
|
|
165
|
-
<th>Сервер / ID</th>
|
|
166
|
-
<th>Владелец / Организация</th>
|
|
167
|
-
<th>Привязка железа (HWID)</th>
|
|
168
|
-
<th>Базовая лицензия (10 вечн.)</th>
|
|
169
|
-
<th>Доп. места и SLA</th>
|
|
170
|
-
<th>Управление</th>
|
|
171
|
-
</tr></thead>
|
|
172
|
-
<tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
173
|
-
</table>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
165
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
166
|
+
<!-- 2. ФРАНШИЗЫ И ГРАФИКИ ПОГАШЕНИЯ (12 МЕС.) -->
|
|
167
|
+
<div data-adm-sec="franchises" style="display:none">
|
|
168
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
169
|
+
<h2 class="sec-title">Региональные франшизы и графики погашения (12 мес.)</h2>
|
|
170
|
+
<div class="sec-controls">
|
|
171
|
+
<input type="search" id="inpFilterFranchises" placeholder="Поиск по региону, компании, ИНН…">
|
|
172
|
+
<select id="selFilterFranchises">
|
|
173
|
+
<option value="all">Все активные</option>
|
|
174
|
+
<option value="pending_deposit">Ожидают залога (15 мин)</option>
|
|
175
|
+
<option value="deposit_paid">Погашение по графику</option>
|
|
176
|
+
<option value="overdue">С просрочкой</option>
|
|
177
|
+
<option value="paid_in_full">Полностью погашены</option>
|
|
178
|
+
<option value="closed">Отменённые / Истёкшие</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="sec-box">
|
|
183
|
+
<table class="sec-table">
|
|
184
|
+
<thead><tr>
|
|
185
|
+
<th>Субъект РФ / ID</th>
|
|
186
|
+
<th>Франчайзи</th>
|
|
187
|
+
<th>Залог (100 тыс. ₽)</th>
|
|
188
|
+
<th>График (12 мес.)</th>
|
|
189
|
+
<th>Оплачено / Остаток</th>
|
|
190
|
+
<th style="text-align:right">Управление</th>
|
|
191
|
+
</tr></thead>
|
|
192
|
+
<tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
193
|
+
</table>
|
|
194
|
+
</div>
|
|
194
195
|
</div>
|
|
195
|
-
</div>
|
|
196
|
-
<div class="table-wrap">
|
|
197
|
-
<table class="t">
|
|
198
|
-
<thead><tr>
|
|
199
|
-
<th>Субъект РФ / ID</th>
|
|
200
|
-
<th>Франчайзи / Контакты</th>
|
|
201
|
-
<th>Начальный взнос (залог 100 тыс. ₽)</th>
|
|
202
|
-
<th>График погашения (12 мес.)</th>
|
|
203
|
-
<th>Финансовая сводка</th>
|
|
204
|
-
<th>Управление</th>
|
|
205
|
-
</tr></thead>
|
|
206
|
-
<tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
207
|
-
</table>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
196
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
197
|
+
<!-- 3. ЗАКАЗЫ И СЧЕТА -->
|
|
198
|
+
<div data-adm-sec="orders" style="display:none">
|
|
199
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
200
|
+
<h2 class="sec-title">Заказы ЮKassa и безналичные счета-договоры</h2>
|
|
201
|
+
<div class="sec-controls">
|
|
202
|
+
<input type="search" id="inpFilterOrders" placeholder="Поиск по заказу, счёту, ИНН…">
|
|
203
|
+
<select id="selFilterOrders">
|
|
204
|
+
<option value="all">Все заказы</option>
|
|
205
|
+
<option value="pending">Ожидают оплаты</option>
|
|
206
|
+
<option value="paid">Оплачены</option>
|
|
207
|
+
<option value="canceled">Отменены</option>
|
|
208
|
+
</select>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="sec-box">
|
|
212
|
+
<table class="sec-table">
|
|
213
|
+
<thead><tr>
|
|
214
|
+
<th>Заказ / Счёт</th>
|
|
215
|
+
<th>Назначение</th>
|
|
216
|
+
<th>Плательщик</th>
|
|
217
|
+
<th>Сумма</th>
|
|
218
|
+
<th>Статус</th>
|
|
219
|
+
<th style="text-align:right">Действия</th>
|
|
220
|
+
</tr></thead>
|
|
221
|
+
<tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</div>
|
|
217
224
|
</div>
|
|
218
|
-
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
219
|
-
<input type="search" id="inpFilterOrders" placeholder="Поиск по номеру заказа, счёту, плательщику, ИНН…" style="width:280px;padding:8px 12px;font-size:13px">
|
|
220
|
-
<select id="selFilterOrders" style="padding:8px 12px;font-size:13px">
|
|
221
|
-
<option value="all">Все заказы</option>
|
|
222
|
-
<option value="pending">Ожидают оплаты</option>
|
|
223
|
-
<option value="paid">Оплачены</option>
|
|
224
|
-
<option value="canceled">Отменены</option>
|
|
225
|
-
</select>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
<div class="table-wrap">
|
|
229
|
-
<table class="t">
|
|
230
|
-
<thead><tr>
|
|
231
|
-
<th>Заказ / Счёт / Дата</th>
|
|
232
|
-
<th>Назначение и объект</th>
|
|
233
|
-
<th>Плательщик / ИНН</th>
|
|
234
|
-
<th class="num">Сумма</th>
|
|
235
|
-
<th>Статус</th>
|
|
236
|
-
<th>Действия</th>
|
|
237
|
-
</tr></thead>
|
|
238
|
-
<tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
239
|
-
</table>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
225
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<
|
|
226
|
+
<!-- 4. КЛИЕНТЫ И РОЛИ BOSS -->
|
|
227
|
+
<div data-adm-sec="users" style="display:none">
|
|
228
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
229
|
+
<h2 class="sec-title">Зарегистрированные клиенты и права администраторов (BOSS)</h2>
|
|
230
|
+
<div class="sec-controls">
|
|
231
|
+
<input type="search" id="inpFilterUsers" placeholder="Поиск по Email, организации, ИНН…">
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="sec-box">
|
|
235
|
+
<table class="sec-table">
|
|
236
|
+
<thead><tr>
|
|
237
|
+
<th>Email / ID</th>
|
|
238
|
+
<th>Роль</th>
|
|
239
|
+
<th>Организация / ИНН</th>
|
|
240
|
+
<th>Реестры клиента</th>
|
|
241
|
+
<th>Оферта (ПЭП)</th>
|
|
242
|
+
<th style="text-align:right">Вход</th>
|
|
243
|
+
</tr></thead>
|
|
244
|
+
<tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
252
247
|
</div>
|
|
253
|
-
</div>
|
|
254
|
-
<div class="table-wrap">
|
|
255
|
-
<table class="t">
|
|
256
|
-
<thead><tr>
|
|
257
|
-
<th>Email / ID</th>
|
|
258
|
-
<th>Роль на сервере</th>
|
|
259
|
-
<th>Организация / ИНН</th>
|
|
260
|
-
<th>Серверов / Франшиз</th>
|
|
261
|
-
<th>Договор-оферта (ПЭП)</th>
|
|
262
|
-
<th>Регистрация / Вход</th>
|
|
263
|
-
</tr></thead>
|
|
264
|
-
<tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
265
|
-
</table>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
248
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
249
|
+
<!-- 5. ТИКЕТЫ ПОДДЕРЖКИ SLA -->
|
|
250
|
+
<div data-adm-sec="tickets" style="display:none">
|
|
251
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
252
|
+
<h2 class="sec-title">Тикеты техподдержки SLA (MAX · Telegram · VK)</h2>
|
|
253
|
+
<div class="sec-controls">
|
|
254
|
+
<select id="selFilterTickets">
|
|
255
|
+
<option value="all">Все обращения</option>
|
|
256
|
+
<option value="open">Открытые</option>
|
|
257
|
+
<option value="answered">Отвеченные</option>
|
|
258
|
+
<option value="closed">Закрытые</option>
|
|
259
|
+
</select>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="sec-box">
|
|
263
|
+
<table class="sec-table">
|
|
264
|
+
<thead><tr>
|
|
265
|
+
<th>Тикет / Канал</th>
|
|
266
|
+
<th>Клиент / Сервер</th>
|
|
267
|
+
<th>SLA</th>
|
|
268
|
+
<th>Переписка</th>
|
|
269
|
+
<th style="text-align:right">Ответ инженера</th>
|
|
270
|
+
</tr></thead>
|
|
271
|
+
<tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
|
|
272
|
+
</table>
|
|
273
|
+
</div>
|
|
275
274
|
</div>
|
|
276
|
-
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
|
277
|
-
<select id="selFilterTickets" style="padding:8px 12px;font-size:13px">
|
|
278
|
-
<option value="all">Все обращения</option>
|
|
279
|
-
<option value="open">Открытые (ожидают ответа)</option>
|
|
280
|
-
<option value="answered">Отвеченные</option>
|
|
281
|
-
<option value="closed">Закрытые</option>
|
|
282
|
-
</select>
|
|
283
|
-
</div>
|
|
284
|
-
</div>
|
|
285
|
-
<div class="table-wrap">
|
|
286
|
-
<table class="t">
|
|
287
|
-
<thead><tr>
|
|
288
|
-
<th>Тикет / Канал</th>
|
|
289
|
-
<th>Клиент / Сервер</th>
|
|
290
|
-
<th>Тариф SLA</th>
|
|
291
|
-
<th>Переписка</th>
|
|
292
|
-
<th>Ответ инженера</th>
|
|
293
|
-
</tr></thead>
|
|
294
|
-
<tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
|
|
295
|
-
</table>
|
|
296
275
|
</div>
|
|
297
276
|
</div>
|
|
298
277
|
</section>
|
|
299
278
|
|
|
300
279
|
<footer>
|
|
301
280
|
<div class="footer-bottom">
|
|
302
|
-
<span>© ODANT ·
|
|
303
|
-
<span>Ключ центра сертификации Ed25519: <code>${esc(publicKey)}
|
|
281
|
+
<span>© ODANT · ${esc(P.domain)}</span>
|
|
282
|
+
<span>Ключ центра сертификации Ed25519: <code>${esc(publicKey.slice(0, 28))}…</code></span>
|
|
304
283
|
</div>
|
|
305
284
|
</footer>
|
|
306
285
|
</div>
|
|
@@ -386,14 +365,14 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
386
365
|
<button class="icon-btn" data-action="close-sched-modal" aria-label="Закрыть">${icon('close')}</button>
|
|
387
366
|
</div>
|
|
388
367
|
<div id="admSchedSummary" style="margin-bottom:12px"></div>
|
|
389
|
-
<div
|
|
390
|
-
<table class="
|
|
368
|
+
<div style="max-height:420px;overflow-y:auto">
|
|
369
|
+
<table class="sec-table">
|
|
391
370
|
<thead><tr>
|
|
392
371
|
<th>Месяц</th>
|
|
393
372
|
<th>Дата платежа</th>
|
|
394
|
-
<th
|
|
373
|
+
<th>Сумма</th>
|
|
395
374
|
<th>Статус</th>
|
|
396
|
-
<th style="text-align:right"
|
|
375
|
+
<th style="text-align:right">Действие</th>
|
|
397
376
|
</tr></thead>
|
|
398
377
|
<tbody id="admSchedBody"></tbody>
|
|
399
378
|
</table>
|
package/src/landing.js
CHANGED
|
@@ -337,50 +337,38 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
337
337
|
<div class="tiers">${renderTiers()}</div>
|
|
338
338
|
</section>
|
|
339
339
|
|
|
340
|
-
<!-- ============ ЛИЧНЫЙ КАБИНЕТ:
|
|
340
|
+
<!-- ============ ЛИЧНЫЙ КАБИНЕТ: ЛЕВОЕ МЕНЮ РАЗДЕЛОВ + ЧИСТЫЕ РЕЕСТРЫ ============ -->
|
|
341
341
|
<section class="stage" id="stage-addons">
|
|
342
|
-
<div class="sec-
|
|
343
|
-
<
|
|
344
|
-
<
|
|
345
|
-
<
|
|
346
|
-
</
|
|
347
|
-
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px">
|
|
348
|
-
<p id="userBadgeBox" style="margin:0"></p>
|
|
349
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
350
|
-
<button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить данные</button>
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
342
|
+
<div class="sec-layout">
|
|
343
|
+
<aside class="sec-nav" aria-label="Разделы Личного кабинета">
|
|
344
|
+
<button type="button" class="sec-nav-item active" data-cab-tab="servers" id="navCabServers">Серверы и устройства (0)</button>
|
|
345
|
+
<button type="button" class="sec-nav-item" data-cab-tab="franchises" id="navCabFranchises">Региональные франшизы (0)</button>
|
|
346
|
+
</aside>
|
|
354
347
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
<
|
|
348
|
+
<div class="sec-main">
|
|
349
|
+
<!-- 1.1 РЕЕСТР СЕРВЕРОВ (УСТРОЙСТВ) WORK -->
|
|
350
|
+
<div id="cabSecServers">
|
|
351
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
352
|
+
<h2 class="sec-title">Серверы и устройства WORK</h2>
|
|
353
|
+
<div class="sec-controls">
|
|
354
|
+
<input type="text" id="inpNewServerName" placeholder="Название нового сервера…">
|
|
355
|
+
<button type="button" class="btn btn-primary btn-sm" data-action="create-server">+ Добавить сервер</button>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="sec-box" id="cabinetServersRegistry"></div>
|
|
366
359
|
</div>
|
|
367
|
-
</div>
|
|
368
|
-
<div id="cabinetServersRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
369
|
-
</div>
|
|
370
360
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<button class="btn btn-primary btn-sm" data-stage="franchise">${icon('map')} Зарезервировать франшизу на карте РФ</button>
|
|
361
|
+
<!-- 1.2 РЕЕСТР РЕГИОНАЛЬНЫХ ФРАНШИЗ -->
|
|
362
|
+
<div id="cabSecFranchises" style="display:none">
|
|
363
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
364
|
+
<h2 class="sec-title">Региональные франшизы</h2>
|
|
365
|
+
<div class="sec-controls">
|
|
366
|
+
<button type="button" class="btn btn-primary btn-sm" data-stage="franchise">+ Выбрать регион на карте РФ</button>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="sec-box" id="cabinetFranchisesRegistry"></div>
|
|
381
370
|
</div>
|
|
382
371
|
</div>
|
|
383
|
-
<div id="cabinetFranchisesRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
384
372
|
</div>
|
|
385
373
|
</section>
|
|
386
374
|
|