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 CHANGED
@@ -16,28 +16,44 @@
16
16
  let activeTab = 'servers';
17
17
  let admCountdownTimer = null;
18
18
 
19
+ function readCookieSession() {
20
+ try {
21
+ const m = String(document.cookie || '').match(/(?:^|;\s*)odant_work_session=([^;]+)/);
22
+ return m?.[1] ? decodeURIComponent(m[1].trim()) : '';
23
+ }
24
+ catch { return ''; }
25
+ }
26
+
19
27
  function getStoredToken() {
20
28
  try {
21
- return sessionStorage.getItem(TOKEN_KEY)
29
+ return localStorage.getItem('odant_work_session')
22
30
  || localStorage.getItem(TOKEN_KEY)
23
- || localStorage.getItem('odant_work_session')
31
+ || sessionStorage.getItem(TOKEN_KEY)
32
+ || sessionStorage.getItem('odant_work_session')
33
+ || readCookieSession()
24
34
  || '';
25
35
  }
26
- catch { return ''; }
36
+ catch { return readCookieSession(); }
27
37
  }
28
- function setStoredToken(tok) {
38
+ function setStoredToken(tok, email = '') {
29
39
  adminToken = tok || '';
30
40
  try {
31
41
  if (tok) {
32
42
  sessionStorage.setItem(TOKEN_KEY, tok);
33
43
  localStorage.setItem(TOKEN_KEY, tok);
34
- if (tok.startsWith('wks_')) {
35
- localStorage.setItem('odant_work_session', tok);
36
- }
44
+ localStorage.setItem('odant_work_session', tok);
45
+ sessionStorage.setItem('odant_work_session', tok);
46
+ document.cookie = `odant_work_session=${encodeURIComponent(tok)}; path=/; max-age=31536000; SameSite=Lax`;
37
47
  }
38
48
  else {
39
49
  sessionStorage.removeItem(TOKEN_KEY);
40
50
  localStorage.removeItem(TOKEN_KEY);
51
+ localStorage.removeItem('odant_work_session');
52
+ sessionStorage.removeItem('odant_work_session');
53
+ document.cookie = 'odant_work_session=; path=/; max-age=0; SameSite=Lax';
54
+ }
55
+ if (email) {
56
+ localStorage.setItem('odant_work_email', email);
41
57
  }
42
58
  }
43
59
  catch { /* приватный режим */ }
@@ -78,6 +94,13 @@
78
94
  $('#adminLoginScreen').style.display = '';
79
95
  $('#adminAppShell').style.display = 'none';
80
96
  if ($('#admLoginError')) $('#admLoginError').textContent = errMsg;
97
+ try {
98
+ const savedEmail = localStorage.getItem('odant_work_email') || '';
99
+ if (savedEmail && $('#admLoginEmail') && !$('#admLoginEmail').value) {
100
+ $('#admLoginEmail').value = savedEmail;
101
+ }
102
+ }
103
+ catch { /* ignore */ }
81
104
  setTimeout(() => $('#admLoginEmail')?.focus(), 50);
82
105
  }
83
106
 
@@ -89,10 +112,21 @@
89
112
  setTimeout(() => $('#admLoginEmail')?.focus(), 50);
90
113
  }
91
114
 
115
+ function moveIndicator() {
116
+ const active = $('#tabs .tab.active');
117
+ const ind = $('#tabInd');
118
+ if (!ind) return;
119
+ if (!active) { ind.style.width = '0px'; return; }
120
+ ind.style.left = active.offsetLeft + 'px';
121
+ ind.style.width = active.offsetWidth + 'px';
122
+ }
123
+
92
124
  function showAdminApp(login = 'BOSS') {
93
125
  $('#adminLoginScreen').style.display = 'none';
94
126
  $('#adminAppShell').style.display = '';
95
- if ($('#admUserLabel')) $('#admUserLabel').textContent = `Выйти (${login})`;
127
+ if ($('#admUserLabel')) $('#admUserLabel').textContent = login;
128
+ if ($('#admProfileEmail')) $('#admProfileEmail').textContent = login;
129
+ requestAnimationFrame(moveIndicator);
96
130
  }
97
131
 
98
132
  async function sendAdminOtpCode() {
@@ -155,7 +189,7 @@
155
189
  if (!user.isAdmin) {
156
190
  throw new Error(`Аккаунт ${user.email} не обладает правами администратора (BOSS) сервера лицензирования.`);
157
191
  }
158
- setStoredToken(user.sessionKey);
192
+ setStoredToken(user.sessionKey, user.email);
159
193
  resetAdmStep();
160
194
  showAdminApp(user.email);
161
195
  await refreshRegistry();
@@ -169,9 +203,9 @@
169
203
  }
170
204
 
171
205
  async function logoutAdmin() {
206
+ $('#admProfileModal')?.classList.remove('open');
172
207
  try { await api('/api/admin/logout', {}); } catch { /* ignore */ }
173
208
  setStoredToken('');
174
- try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
175
209
  resetAdmStep();
176
210
  showLoginGate('');
177
211
  }
@@ -186,6 +220,7 @@
186
220
  try {
187
221
  const me = await api('/api/admin/me');
188
222
  if (me.authenticated) {
223
+ setStoredToken(saved, me.email || me.login || '');
189
224
  showAdminApp(me.login || 'admin');
190
225
  await refreshRegistry();
191
226
  }
@@ -201,7 +236,7 @@
201
236
  /* ================= Вкладки и форматирование ================= */
202
237
  function setTab(tab) {
203
238
  activeTab = tab || 'servers';
204
- $$('#admTopTabs [data-adm-tab]').forEach(b => b.classList.toggle('active', b.dataset.admTab === activeTab));
239
+ $$('[data-adm-tab]').forEach(b => b.classList.toggle('active', b.dataset.admTab === activeTab));
205
240
  $$('[data-adm-sec]').forEach(sec => {
206
241
  sec.style.display = sec.dataset.admSec === activeTab ? '' : 'none';
207
242
  });
@@ -265,34 +300,13 @@
265
300
  const users = data.users || [];
266
301
 
267
302
  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
303
  const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
274
304
 
275
- if ($('#tabCntServers')) $('#tabCntServers').textContent = String(licenses.length);
276
- if ($('#tabCntFranchises')) $('#tabCntFranchises').textContent = String(activeFranchises.length);
277
- if ($('#tabCntOrders')) $('#tabCntOrders').textContent = String(orders.length);
278
- if ($('#tabCntUsers')) $('#tabCntUsers').textContent = String(users.length);
279
- if ($('#tabCntTickets')) $('#tabCntTickets').textContent = String(openTickets.length);
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
- }
305
+ if ($('#navAdmServers')) $('#navAdmServers').textContent = `Серверы и лицензии (${licenses.length})`;
306
+ if ($('#navAdmFranchises')) $('#navAdmFranchises').textContent = `Франшизы (${activeFranchises.length})`;
307
+ if ($('#navAdmOrders')) $('#navAdmOrders').textContent = `Заказы и счета (${orders.length})`;
308
+ if ($('#navAdmUsers')) $('#navAdmUsers').textContent = `Клиенты и роли (${users.length})`;
309
+ if ($('#navAdmTickets')) $('#navAdmTickets').textContent = `Тикеты поддержки (${openTickets.length})`;
296
310
 
297
311
  renderServersTable(licenses);
298
312
  renderFranchisesTable(franchises);
@@ -364,10 +378,8 @@
364
378
  <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
379
  </div>
366
380
  </td>
367
- <td>
368
- <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-start">
369
- <button class="btn btn-primary btn-sm" data-cert="${esc(lid)}">Файлы .lic.json (${(l.licenseFiles || []).length})</button>
370
- </div>
381
+ <td style="text-align:right">
382
+ <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
383
  </td>
372
384
  </tr>`;
373
385
  }).join('') || '<tr><td colspan="6" class="muted">Серверов по заданному фильтру не найдено</td></tr>';
@@ -913,6 +925,8 @@
913
925
  case 'adm-send-code': sendAdminOtpCode(); break;
914
926
  case 'adm-verify-code': verifyAdminOtpCode(); break;
915
927
  case 'adm-reset-step': resetAdmStep(); break;
928
+ case 'open-adm-profile': $('#admProfileModal')?.classList.add('open'); break;
929
+ case 'close-adm-profile': $('#admProfileModal')?.classList.remove('open'); break;
916
930
  case 'admin-logout': logoutAdmin(); break;
917
931
  case 'registry': refreshRegistry(); break;
918
932
  case 'open-create-server-modal': $('#admCreateServerModal')?.classList.add('open'); break;
@@ -949,12 +963,14 @@
949
963
  $$('.modal.open').forEach(m => m.classList.remove('open'));
950
964
  }
951
965
  });
966
+ window.addEventListener('resize', moveIndicator);
952
967
  }
953
968
 
954
969
  function start() {
955
970
  applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
956
971
  bind();
957
972
  setTab('servers');
973
+ if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
958
974
  checkAdminSession();
959
975
  }
960
976
 
@@ -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;