work-license-server 1.0.22 → 1.0.23
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 +639 -180
- package/assets/landing.css +158 -0
- package/package.json +1 -1
- package/src/admin.js +219 -73
- package/src/server.js +159 -17
- package/src/store.js +246 -9
- package/src/yookassa.js +9 -4
package/assets/admin.js
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
let lastAdminData = null;
|
|
14
14
|
let currentCertLic = null;
|
|
15
15
|
let currentScheduleResId = '';
|
|
16
|
-
let activeTab = '
|
|
16
|
+
let activeTab = 'clients';
|
|
17
17
|
let admCountdownTimer = null;
|
|
18
|
+
const expandedClients = new Set();
|
|
19
|
+
const expandedFranchisers = new Set();
|
|
18
20
|
|
|
19
21
|
function readCookieSession() {
|
|
20
22
|
try {
|
|
@@ -239,7 +241,7 @@
|
|
|
239
241
|
|
|
240
242
|
/* ================= Вкладки и форматирование ================= */
|
|
241
243
|
function setTab(tab) {
|
|
242
|
-
activeTab = tab || '
|
|
244
|
+
activeTab = tab || 'clients';
|
|
243
245
|
$$('[data-adm-tab]').forEach(b => b.classList.toggle('active', b.dataset.admTab === activeTab));
|
|
244
246
|
$$('[data-adm-sec]').forEach(sec => {
|
|
245
247
|
sec.style.display = sec.dataset.admSec === activeTab ? '' : 'none';
|
|
@@ -296,6 +298,147 @@
|
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
function cleanOrgName(raw = '') {
|
|
302
|
+
const s = String(raw || '').trim();
|
|
303
|
+
if (!s || s.includes('@') || s === 'Организация ПАК WORK' || s === 'Клиент WORK' || s === 'Партнёр ODANT-WORK')
|
|
304
|
+
return '';
|
|
305
|
+
return s;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function buildClientGroups(users = [], licenses = []) {
|
|
309
|
+
const map = new Map();
|
|
310
|
+
|
|
311
|
+
for (const u of users) {
|
|
312
|
+
if (!u) continue;
|
|
313
|
+
const email = String(u.email || '').trim().toLowerCase();
|
|
314
|
+
const key = email || String(u.userId || '').toLowerCase();
|
|
315
|
+
if (!key) continue;
|
|
316
|
+
map.set(key, {
|
|
317
|
+
key,
|
|
318
|
+
email: u.email || key,
|
|
319
|
+
userId: u.userId || '',
|
|
320
|
+
isAdmin: Boolean(u.isAdmin),
|
|
321
|
+
name: (u.name && !String(u.name).includes('@')) ? String(u.name).trim() : '',
|
|
322
|
+
position: String(u.position || u.signerPosition || '').trim(),
|
|
323
|
+
companyName: cleanOrgName(u.companyName),
|
|
324
|
+
inn: String(u.inn || '').trim(),
|
|
325
|
+
kpp: String(u.kpp || '').trim(),
|
|
326
|
+
phone: String(u.phone || '').trim(),
|
|
327
|
+
ofertaAccepted: Boolean(u.ofertaAccepted),
|
|
328
|
+
ofertaSignatureId: String(u.ofertaSignatureId || '').trim(),
|
|
329
|
+
createdAt: Number(u.createdAt || 0),
|
|
330
|
+
lastLoginAt: Number(u.lastLoginAt || u.createdAt || 0),
|
|
331
|
+
servers: [],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const groupsList = () => Array.from(map.values());
|
|
336
|
+
|
|
337
|
+
for (const lic of licenses) {
|
|
338
|
+
if (!lic) continue;
|
|
339
|
+
const licEmail = String(lic.ownerEmail || lic.customer?.email || '').trim().toLowerCase();
|
|
340
|
+
let grp = licEmail ? map.get(licEmail) : null;
|
|
341
|
+
if (!grp && lic.ownerAccountId) {
|
|
342
|
+
grp = groupsList().find(g => g.userId && g.userId === lic.ownerAccountId) || null;
|
|
343
|
+
}
|
|
344
|
+
if (!grp) {
|
|
345
|
+
const key = licEmail || String(lic.licenseId || '').toLowerCase();
|
|
346
|
+
grp = {
|
|
347
|
+
key,
|
|
348
|
+
email: licEmail || lic.licenseId,
|
|
349
|
+
userId: lic.ownerAccountId || '',
|
|
350
|
+
isAdmin: false,
|
|
351
|
+
name: '',
|
|
352
|
+
position: '',
|
|
353
|
+
companyName: cleanOrgName(lic.customer?.name),
|
|
354
|
+
inn: String(lic.customer?.inn || '').trim(),
|
|
355
|
+
kpp: '',
|
|
356
|
+
phone: String(lic.customer?.phone || '').trim(),
|
|
357
|
+
ofertaAccepted: false,
|
|
358
|
+
ofertaSignatureId: '',
|
|
359
|
+
createdAt: Number(lic.issuedAt || 0),
|
|
360
|
+
lastLoginAt: Number(lic.updatedAt || lic.issuedAt || 0),
|
|
361
|
+
servers: [],
|
|
362
|
+
};
|
|
363
|
+
map.set(key, grp);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
if (!grp.companyName && cleanOrgName(lic.customer?.name))
|
|
367
|
+
grp.companyName = cleanOrgName(lic.customer?.name);
|
|
368
|
+
if (!grp.inn && lic.customer?.inn)
|
|
369
|
+
grp.inn = String(lic.customer.inn).trim();
|
|
370
|
+
if (!grp.phone && lic.customer?.phone)
|
|
371
|
+
grp.phone = String(lic.customer.phone).trim();
|
|
372
|
+
}
|
|
373
|
+
if (!grp.servers.some(s => s.licenseId === lic.licenseId)) {
|
|
374
|
+
grp.servers.push(lic);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const result = groupsList();
|
|
379
|
+
for (const g of result) {
|
|
380
|
+
g.servers.sort((a, b) => Number(a.issuedAt || 0) - Number(b.issuedAt || 0));
|
|
381
|
+
}
|
|
382
|
+
result.sort((a, b) => {
|
|
383
|
+
if (a.isAdmin !== b.isAdmin) return a.isAdmin ? -1 : 1;
|
|
384
|
+
return (b.lastLoginAt || b.createdAt || 0) - (a.lastLoginAt || a.createdAt || 0);
|
|
385
|
+
});
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function buildFranchiserGroups(franchises = [], users = []) {
|
|
390
|
+
const userByEmail = new Map();
|
|
391
|
+
const userById = new Map();
|
|
392
|
+
for (const u of users) {
|
|
393
|
+
if (!u) continue;
|
|
394
|
+
if (u.email) userByEmail.set(String(u.email).trim().toLowerCase(), u);
|
|
395
|
+
if (u.userId) userById.set(String(u.userId), u);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const map = new Map();
|
|
399
|
+
for (const f of franchises) {
|
|
400
|
+
if (!f) continue;
|
|
401
|
+
const u = (f.email && userByEmail.get(String(f.email).trim().toLowerCase()))
|
|
402
|
+
|| (f.ownerAccountId && userById.get(String(f.ownerAccountId)))
|
|
403
|
+
|| null;
|
|
404
|
+
const email = String(f.email || u?.email || '').trim().toLowerCase();
|
|
405
|
+
const key = email || String(f.ownerAccountId || f.inn || f.reservationId || '').toLowerCase();
|
|
406
|
+
if (!key) continue;
|
|
407
|
+
|
|
408
|
+
let grp = map.get(key);
|
|
409
|
+
if (!grp) {
|
|
410
|
+
grp = {
|
|
411
|
+
key,
|
|
412
|
+
email: f.email || u?.email || key,
|
|
413
|
+
userId: f.ownerAccountId || u?.userId || '',
|
|
414
|
+
name: (u?.name && !String(u.name).includes('@')) ? String(u.name).trim() : '',
|
|
415
|
+
companyName: cleanOrgName(f.companyName || u?.companyName),
|
|
416
|
+
inn: String(f.inn || u?.inn || '').trim(),
|
|
417
|
+
phone: String(f.phone || u?.phone || '').trim(),
|
|
418
|
+
items: [],
|
|
419
|
+
};
|
|
420
|
+
map.set(key, grp);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
if (!grp.companyName && cleanOrgName(f.companyName)) grp.companyName = cleanOrgName(f.companyName);
|
|
424
|
+
if (!grp.inn && f.inn) grp.inn = String(f.inn).trim();
|
|
425
|
+
if (!grp.phone && f.phone) grp.phone = String(f.phone).trim();
|
|
426
|
+
}
|
|
427
|
+
grp.items.push(f);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const result = Array.from(map.values());
|
|
431
|
+
for (const g of result) {
|
|
432
|
+
g.items.sort((a, b) => Number(b.createdAt || 0) - Number(a.createdAt || 0));
|
|
433
|
+
}
|
|
434
|
+
result.sort((a, b) => {
|
|
435
|
+
const aMax = Math.max(0, ...a.items.map(x => Number(x.createdAt || 0)));
|
|
436
|
+
const bMax = Math.max(0, ...b.items.map(x => Number(x.createdAt || 0)));
|
|
437
|
+
return bMax - aMax;
|
|
438
|
+
});
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
299
442
|
function renderAdminPanel(data = {}) {
|
|
300
443
|
const licenses = data.licenses || [];
|
|
301
444
|
const franchises = data.franchises || data.reservations || [];
|
|
@@ -303,104 +446,232 @@
|
|
|
303
446
|
const tickets = data.tickets || [];
|
|
304
447
|
const users = data.users || [];
|
|
305
448
|
|
|
449
|
+
const clientGroups = buildClientGroups(users, licenses);
|
|
306
450
|
const activeFranchises = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
451
|
+
const activeFranchiserGroups = buildFranchiserGroups(activeFranchises, users);
|
|
307
452
|
const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
|
|
308
453
|
|
|
309
|
-
if ($('#
|
|
310
|
-
if ($('#
|
|
454
|
+
if ($('#navAdmClients')) $('#navAdmClients').textContent = `Клиенты (${clientGroups.length})`;
|
|
455
|
+
if ($('#navAdmFranchisers')) $('#navAdmFranchisers').textContent = `Франчайзеры (${activeFranchiserGroups.length})`;
|
|
311
456
|
if ($('#navAdmOrders')) $('#navAdmOrders').textContent = `Заказы и счета (${orders.length})`;
|
|
312
|
-
if ($('#navAdmUsers')) $('#navAdmUsers').textContent = `Клиенты и администраторы (${users.length})`;
|
|
313
457
|
if ($('#navAdmTickets')) $('#navAdmTickets').textContent = `Тикеты поддержки (${openTickets.length})`;
|
|
314
458
|
if ($('#admDataDirLabel') && data.dataDir) {
|
|
315
459
|
$('#admDataDirLabel').textContent = `Папка БД: ${data.dataDir}`;
|
|
316
460
|
}
|
|
317
461
|
|
|
318
|
-
|
|
319
|
-
|
|
462
|
+
renderClientsPanels(clientGroups);
|
|
463
|
+
renderFranchisersPanels(franchises, users);
|
|
320
464
|
renderOrdersTable(orders);
|
|
321
|
-
renderUsersTable(users);
|
|
322
465
|
renderTicketsTable(tickets);
|
|
466
|
+
renderPortalSettings(data.settings || {});
|
|
323
467
|
startAdminCountdowns();
|
|
324
468
|
}
|
|
325
469
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
470
|
+
function renderPortalSettings(settings = {}) {
|
|
471
|
+
const smtp = settings.smtp || {};
|
|
472
|
+
const yk = settings.yookassa || {};
|
|
473
|
+
const bots = settings.bots || {};
|
|
474
|
+
const legal = settings.legal || {};
|
|
475
|
+
|
|
476
|
+
if ($('#navBadgeSmtp')) {
|
|
477
|
+
$('#navBadgeSmtp').textContent = smtp.connected ? 'активна' : 'не задана';
|
|
478
|
+
$('#navBadgeSmtp').className = smtp.connected ? 'tag ok' : 'tag';
|
|
479
|
+
}
|
|
480
|
+
if ($('#admSmtpStatusTag')) {
|
|
481
|
+
$('#admSmtpStatusTag').textContent = smtp.connected
|
|
482
|
+
? `✓ Подключена (${smtp.mailFile || 'mail.json'})`
|
|
483
|
+
: 'Не настроена';
|
|
484
|
+
$('#admSmtpStatusTag').className = smtp.connected ? 'tag ok' : 'tag';
|
|
485
|
+
}
|
|
486
|
+
if ($('#admSmtpHost')) $('#admSmtpHost').value = smtp.host || '';
|
|
487
|
+
if ($('#admSmtpPort')) $('#admSmtpPort').value = smtp.port || 465;
|
|
488
|
+
if ($('#admSmtpSecure')) $('#admSmtpSecure').checked = smtp.secure !== false;
|
|
489
|
+
if ($('#admSmtpUser')) $('#admSmtpUser').value = smtp.user || '';
|
|
490
|
+
if ($('#admSmtpPass')) $('#admSmtpPass').value = smtp.pass || '';
|
|
491
|
+
if ($('#admSmtpFrom')) $('#admSmtpFrom').value = smtp.fromEmail || smtp.user || '';
|
|
492
|
+
if ($('#admSmtpSenderName')) $('#admSmtpSenderName').value = smtp.senderName || 'ODANT-WORK';
|
|
493
|
+
|
|
494
|
+
if ($('#admYookassaStatusTag')) {
|
|
495
|
+
$('#admYookassaStatusTag').textContent = yk.live ? '✓ Боевой режим (API v3)' : 'Режим песочницы';
|
|
496
|
+
$('#admYookassaStatusTag').className = yk.live ? 'tag ok' : 'tag';
|
|
497
|
+
}
|
|
498
|
+
if ($('#admYkShopId')) $('#admYkShopId').value = yk.shopId || '';
|
|
499
|
+
if ($('#admYkSecretKey')) $('#admYkSecretKey').value = yk.secretKey || '';
|
|
500
|
+
if ($('#admYkSendReceipt')) $('#admYkSendReceipt').checked = Boolean(yk.sendReceipt);
|
|
501
|
+
|
|
502
|
+
if ($('#admBadgeTgBot')) {
|
|
503
|
+
const tgName = bots.telegramUsername ? `@${bots.telegramUsername}` : (bots.telegramToken ? 'токен задан' : 'не задан');
|
|
504
|
+
$('#admBadgeTgBot').textContent = `TG: ${tgName}`;
|
|
505
|
+
$('#admBadgeTgBot').className = (bots.telegramUsername || bots.telegramToken) ? 'tag ok' : 'tag';
|
|
506
|
+
}
|
|
507
|
+
if ($('#admBadgeMaxBot')) {
|
|
508
|
+
const maxName = bots.maxUsername ? `@${bots.maxUsername}` : (bots.maxToken ? 'токен задан' : 'не задан');
|
|
509
|
+
$('#admBadgeMaxBot').textContent = `MAX: ${maxName}`;
|
|
510
|
+
$('#admBadgeMaxBot').className = (bots.maxUsername || bots.maxToken) ? 'tag ok' : 'tag';
|
|
511
|
+
}
|
|
512
|
+
if ($('#admBotTgToken')) $('#admBotTgToken').value = bots.telegramToken || '';
|
|
513
|
+
if ($('#admBotTgUsername')) $('#admBotTgUsername').value = bots.telegramUsername || '';
|
|
514
|
+
if ($('#admBotMaxToken')) $('#admBotMaxToken').value = bots.maxToken || '';
|
|
515
|
+
if ($('#admBotMaxUsername')) $('#admBotMaxUsername').value = bots.maxUsername || '';
|
|
516
|
+
|
|
517
|
+
if ($('#admLegalCompany')) $('#admLegalCompany').value = legal.companyName || '';
|
|
518
|
+
if ($('#admLegalAuthor')) $('#admLegalAuthor').value = legal.authorName || '';
|
|
519
|
+
if ($('#admLegalStatus')) $('#admLegalStatus').value = legal.legalStatus || '';
|
|
520
|
+
if ($('#admLegalInn')) $('#admLegalInn').value = legal.inn || '';
|
|
521
|
+
if ($('#admLegalOgrn')) $('#admLegalOgrn').value = legal.ogrn || '';
|
|
522
|
+
if ($('#admLegalEmail')) $('#admLegalEmail').value = legal.supportEmail || '';
|
|
523
|
+
if ($('#admLegalPhone')) $('#admLegalPhone').value = legal.phone || '';
|
|
524
|
+
if ($('#admLegalAddress')) $('#admLegalAddress').value = legal.address || '';
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* 1. Раскрывающиеся панели клиентов и вложенные серверы/лицензии */
|
|
528
|
+
function renderClientsPanels(clientGroups = []) {
|
|
529
|
+
const root = $('#admClientsList');
|
|
530
|
+
if (!root) return;
|
|
531
|
+
const q = ($('#inpFilterClients')?.value || '').trim().toLowerCase();
|
|
532
|
+
const mode = $('#selFilterClients')?.value || 'all';
|
|
533
|
+
const autoExpand = Boolean(q) || mode !== 'all';
|
|
534
|
+
|
|
535
|
+
const filtered = clientGroups.filter(c => {
|
|
536
|
+
if (mode === 'admins' && !c.isAdmin) return false;
|
|
537
|
+
if (mode === 'base_active' && !c.servers.some(l => l.baseActivated)) return false;
|
|
538
|
+
if (mode === 'base_inactive' && !(c.servers.length === 0 || c.servers.some(l => !l.baseActivated))) return false;
|
|
539
|
+
if (mode === 'hwid_bound' && !c.servers.some(l => Boolean(l.hwidBound || l.hardware?.hwidBound || l.hardware?.nodeFingerprint))) return false;
|
|
338
540
|
if (!q) return true;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return
|
|
541
|
+
|
|
542
|
+
const clientHay = [c.email, c.userId, c.name, c.companyName, c.inn, c.kpp, c.phone].join(' ').toLowerCase();
|
|
543
|
+
if (clientHay.includes(q)) return true;
|
|
544
|
+
|
|
545
|
+
return c.servers.some(l => [
|
|
546
|
+
l.licenseId, l.serverName, l.hardware?.hwid, l.hardware?.nodeFingerprint,
|
|
547
|
+
].join(' ').toLowerCase().includes(q));
|
|
344
548
|
});
|
|
345
549
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
550
|
+
if (!filtered.length) {
|
|
551
|
+
root.innerHTML = '<div class="sec-box muted">Клиентов по заданному фильтру не найдено</div>';
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
root.innerHTML = filtered.map(c => {
|
|
556
|
+
const isOpen = autoExpand || expandedClients.has(c.key);
|
|
557
|
+
const activeBaseCount = c.servers.filter(s => s.baseActivated).length;
|
|
558
|
+
const totalSeats = c.servers.reduce((sum, s) => sum + Number(s.totalSeats || 0), 0);
|
|
559
|
+
const addonSeatsTotal = c.servers.reduce((sum, s) => sum + Number(s.addonSeats || 0), 0);
|
|
560
|
+
|
|
561
|
+
const subParts = [
|
|
562
|
+
c.name ? `${c.name}${c.position ? ` (${c.position})` : ''}` : '',
|
|
563
|
+
c.companyName,
|
|
564
|
+
c.inn ? `ИНН ${c.inn}` : '',
|
|
565
|
+
].filter(Boolean);
|
|
566
|
+
|
|
567
|
+
const metaItems = [];
|
|
568
|
+
if (c.userId) metaItems.push(`<span>ID: <code>${esc(c.userId)}</code></span>`);
|
|
569
|
+
if (c.name) metaItems.push(`<span>Физ. лицо: <b>${esc(c.name)}</b>${c.position ? ` (${esc(c.position)})` : ''}</span>`);
|
|
570
|
+
if (c.companyName || c.inn) {
|
|
571
|
+
metaItems.push(`<span>Юр. лицо: <b>${esc(c.companyName || '—')}</b>${c.inn ? ` · ИНН <code>${esc(c.inn)}</code>` : ''}${c.kpp ? ` · КПП <code>${esc(c.kpp)}</code>` : ''}</span>`);
|
|
572
|
+
}
|
|
573
|
+
if (c.phone) metaItems.push(`<span>Тел.: <code>${esc(c.phone)}</code></span>`);
|
|
574
|
+
metaItems.push(`<span>Оферта: ${c.ofertaAccepted ? `<b style="color:var(--ok)">✓ ПЭП</b> (<code>${esc(c.ofertaSignatureId)}</code>)` : 'не подписана'}</span>`);
|
|
575
|
+
if (c.lastLoginAt) metaItems.push(`<span>Вход: ${fmtDateTime(c.lastLoginAt)}</span>`);
|
|
576
|
+
|
|
577
|
+
const serversRowsHtml = c.servers.map(l => {
|
|
578
|
+
const lid = l.licenseId;
|
|
579
|
+
const srvName = l.serverName || 'Сервер WORK';
|
|
580
|
+
const hwidBound = Boolean(l.hwidBound || l.hardware?.hwidBound || l.hardware?.nodeFingerprint);
|
|
581
|
+
const hwid = l.hardware?.hwid || '—';
|
|
582
|
+
const fp = l.hardware?.nodeFingerprint || '';
|
|
583
|
+
|
|
584
|
+
const baseHtml = l.baseActivated
|
|
585
|
+
? `<span class="tag ok">✓ Активна (${l.baseSeats || 10} вечн. + ИИ)</span>
|
|
586
|
+
<div style="margin-top:4px"><button class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px;color:var(--danger)" data-adm-base="${esc(lid)}" data-activate="0">Отозвать базовую</button></div>`
|
|
587
|
+
: `<span class="tag">Не активирована (1 сессия)</span>
|
|
588
|
+
<div style="margin-top:4px"><button class="btn btn-ok btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-base="${esc(lid)}" data-activate="1">Активировать базовую</button></div>`;
|
|
589
|
+
|
|
590
|
+
const hwidHtml = hwidBound
|
|
591
|
+
? `<span class="tag ok">Привязан</span> <code>${esc(hwid)}</code>
|
|
592
|
+
${fp ? `<div class="small muted">Узел: <code>${esc(fp.slice(0, 18))}…</code></div>` : ''}
|
|
593
|
+
<div style="margin-top:4px"><button class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-reset-hwid="${esc(lid)}">Сбросить привязку</button></div>`
|
|
594
|
+
: `<span class="small muted">Ожидает привязки токена</span><div class="small muted"><code>${esc(hwid)}</code></div>`;
|
|
595
|
+
|
|
596
|
+
return `<tr>
|
|
597
|
+
<td>
|
|
598
|
+
<b>${esc(srvName)}</b>
|
|
599
|
+
<div class="small muted"><code>${esc(lid)}</code> · создан ${fmtDate(l.issuedAt)}</div>
|
|
600
|
+
<div style="margin-top:4px;display:flex;gap:6px;flex-wrap:wrap">
|
|
601
|
+
<button class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-copy-tok="${esc(lid)}">Скопировать токен owk1</button>
|
|
602
|
+
</div>
|
|
603
|
+
</td>
|
|
604
|
+
<td>${hwidHtml}</td>
|
|
605
|
+
<td>${baseHtml}</td>
|
|
606
|
+
<td>
|
|
607
|
+
<div>Всего сессий: <b>${l.totalSeats}</b> (+<b>${l.addonSeats || 0}</b> доп.)</div>
|
|
608
|
+
<div class="small muted">SLA: ${esc(l.support?.tierName || 'Enterprise')} (пакетов: ${(l.activeAddons || []).length})</div>
|
|
609
|
+
<div style="margin-top:4px">
|
|
610
|
+
<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>
|
|
611
|
+
</div>
|
|
612
|
+
</td>
|
|
613
|
+
<td style="text-align:right">
|
|
614
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 10px;font-size:12px" data-cert="${esc(lid)}">.lic.json (${(l.licenseFiles || []).length})</button>
|
|
615
|
+
</td>
|
|
616
|
+
</tr>`;
|
|
617
|
+
}).join('');
|
|
365
618
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
<
|
|
619
|
+
const serversTableHtml = c.servers.length
|
|
620
|
+
? `<table class="sec-table">
|
|
621
|
+
<thead><tr>
|
|
622
|
+
<th>Сервер / ID</th>
|
|
623
|
+
<th>Железо (HWID)</th>
|
|
624
|
+
<th>Базовая лицензия</th>
|
|
625
|
+
<th>Доп. места и SLA</th>
|
|
626
|
+
<th style="text-align:right">Файлы</th>
|
|
627
|
+
</tr></thead>
|
|
628
|
+
<tbody>${serversRowsHtml}</tbody>
|
|
629
|
+
</table>`
|
|
630
|
+
: `<div class="small muted" style="padding:8px 4px">У клиента пока нет серверов. Нажмите «+ Добавить сервер», чтобы выпустить лицензию.</div>`;
|
|
631
|
+
|
|
632
|
+
return `<div class="adm-acc-card${isOpen ? ' open' : ''}" data-client-card="${esc(c.key)}">
|
|
633
|
+
<div class="adm-acc-head" data-adm-toggle-client="${esc(c.key)}">
|
|
634
|
+
<div class="adm-acc-ident">
|
|
635
|
+
<span class="adm-acc-chev">▸</span>
|
|
636
|
+
<div>
|
|
637
|
+
<div class="adm-acc-email">${esc(c.email)}</div>
|
|
638
|
+
${subParts.length ? `<div class="adm-acc-sub">${esc(subParts.join(' · '))}</div>` : ''}
|
|
639
|
+
</div>
|
|
372
640
|
</div>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
<td>${hwidHtml}</td>
|
|
380
|
-
<td>${baseHtml}</td>
|
|
381
|
-
<td>
|
|
382
|
-
<div>Всего сессий: <b>${l.totalSeats}</b> (+<b>${l.addonSeats || 0}</b> доп.)</div>
|
|
383
|
-
<div class="small muted">SLA: ${esc(l.support?.tierName || 'Enterprise')} (пакетов: ${(l.activeAddons || []).length})</div>
|
|
384
|
-
<div style="margin-top:4px">
|
|
385
|
-
<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>
|
|
641
|
+
<div class="adm-acc-badges">
|
|
642
|
+
${c.isAdmin ? '<span class="tag ok">Администратор</span>' : ''}
|
|
643
|
+
${c.ofertaAccepted ? `<span class="tag ok" title="ПЭП: ${esc(c.ofertaSignatureId)}">✓ ПЭП</span>` : ''}
|
|
644
|
+
<span class="tag">Серверов: <b>${c.servers.length}</b></span>
|
|
645
|
+
<span class="tag ${activeBaseCount > 0 ? 'ok' : ''}">Сессий: <b>${totalSeats}</b>${addonSeatsTotal > 0 ? ` (+${addonSeatsTotal})` : ''}</span>
|
|
646
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:4px 10px;font-size:12px" data-adm-add-srv-email="${esc(c.email)}" data-adm-next-num="${c.servers.length + 1}">+ Сервер</button>
|
|
386
647
|
</div>
|
|
387
|
-
</
|
|
388
|
-
<
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
648
|
+
</div>
|
|
649
|
+
<div class="adm-acc-body">
|
|
650
|
+
<div class="adm-profile-strip">
|
|
651
|
+
<div class="adm-profile-meta">${metaItems.join('')}</div>
|
|
652
|
+
<div class="adm-profile-actions">
|
|
653
|
+
${c.isAdmin
|
|
654
|
+
? `<button type="button" class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-toggle-admin="${esc(c.userId || c.email)}" data-make-admin="0">Снять права админа</button>`
|
|
655
|
+
: `<button type="button" class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-toggle-admin="${esc(c.userId || c.email)}" data-make-admin="1">Сделать администратором</button>`}
|
|
656
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:4px 10px;font-size:12px" data-adm-add-srv-email="${esc(c.email)}" data-adm-next-num="${c.servers.length + 1}">+ Добавить сервер</button>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
${serversTableHtml}
|
|
660
|
+
</div>
|
|
661
|
+
</div>`;
|
|
662
|
+
}).join('');
|
|
393
663
|
}
|
|
394
664
|
|
|
395
|
-
/* 2.
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
if (!
|
|
399
|
-
const q = ($('#
|
|
400
|
-
const mode = $('#
|
|
665
|
+
/* 2. Раскрывающиеся панели франчайзеров и вложенные регионы со статусами */
|
|
666
|
+
function renderFranchisersPanels(franchises = [], users = []) {
|
|
667
|
+
const root = $('#admFranchisersList');
|
|
668
|
+
if (!root) return;
|
|
669
|
+
const q = ($('#inpFilterFranchisers')?.value || '').trim().toLowerCase();
|
|
670
|
+
const mode = $('#selFilterFranchisers')?.value || 'all';
|
|
671
|
+
const autoExpand = Boolean(q) || mode !== 'all';
|
|
401
672
|
const now = Date.now();
|
|
402
673
|
|
|
403
|
-
const
|
|
674
|
+
const filteredFranchises = franchises.filter(f => {
|
|
404
675
|
if (!f) return false;
|
|
405
676
|
const isClosed = f.status === 'canceled' || f.status === 'expired';
|
|
406
677
|
const remaining = Number(f.remainingFranchiseFeeRub ?? 0);
|
|
@@ -413,74 +684,147 @@
|
|
|
413
684
|
if (!q) return true;
|
|
414
685
|
const hay = [
|
|
415
686
|
f.reservationId, f.regionName, f.regionCode, f.district,
|
|
416
|
-
f.companyName, f.inn, f.email,
|
|
687
|
+
f.companyName, f.inn, f.email, f.phone,
|
|
417
688
|
].join(' ').toLowerCase();
|
|
418
689
|
return hay.includes(q);
|
|
419
690
|
});
|
|
420
691
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const paidRub = Number(f.totalPaidRub ?? (f.depositPaid ? depositRub : 0));
|
|
427
|
-
const remRub = Number(f.remainingFranchiseFeeRub ?? Math.max(0, totalFeeRub - paidRub));
|
|
428
|
-
const paidMonths = Number(f.paidMonthsCount || 0);
|
|
429
|
-
const overdueMonths = Number(f.overdueMonthsCount || 0);
|
|
430
|
-
const pct = totalFeeRub > 0 ? Math.min(100, Math.round((paidRub / totalFeeRub) * 100)) : 0;
|
|
431
|
-
|
|
432
|
-
const dl = Number(f.depositDeadlineAt || ((f.createdAt || now) + 15 * 60_000));
|
|
433
|
-
const remSec = Math.max(0, Math.floor((dl - now) / 1000));
|
|
434
|
-
|
|
435
|
-
let depositHtml = '';
|
|
436
|
-
if (isClosed) {
|
|
437
|
-
depositHtml = `<span class="tag bad">${f.status === 'expired' ? 'Истёк (15 мин)' : 'Отменён'}</span>`;
|
|
438
|
-
}
|
|
439
|
-
else if (f.depositPaid) {
|
|
440
|
-
depositHtml = `<span class="tag ok">✓ Оплачен (${rub(depositRub)})</span><div class="small muted">${fmtDateTime(f.paidAt)}</div>`;
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
depositHtml = `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Таймер 15 мин: <b data-adm-deadline="${dl}">${fmtCountdown(remSec)}</b></span>
|
|
444
|
-
<div style="margin-top:4px"><button class="btn btn-ok btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-pay-deposit="${esc(rid)}">Зачесть залог (${rub(depositRub)})</button></div>`;
|
|
445
|
-
}
|
|
692
|
+
const groups = buildFranchiserGroups(filteredFranchises, users);
|
|
693
|
+
if (!groups.length) {
|
|
694
|
+
root.innerHTML = '<div class="sec-box muted">Франчайзеров по заданному фильтру не найдено</div>';
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
446
697
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
698
|
+
root.innerHTML = groups.map(fg => {
|
|
699
|
+
const isOpen = autoExpand || expandedFranchisers.has(fg.key);
|
|
700
|
+
let totalPaidGroup = 0;
|
|
701
|
+
let totalRemGroup = 0;
|
|
702
|
+
let pendingDepCount = 0;
|
|
703
|
+
let overdueRegionsCount = 0;
|
|
704
|
+
let activeSchedCount = 0;
|
|
705
|
+
let paidFullCount = 0;
|
|
706
|
+
|
|
707
|
+
for (const f of fg.items) {
|
|
708
|
+
const isClosed = f.status === 'canceled' || f.status === 'expired';
|
|
709
|
+
const depositRub = Number(f.nonRefundableDepositRub || 100_000);
|
|
710
|
+
const totalFeeRub = Number(f.franchiseFeeRub || 500_000);
|
|
711
|
+
const paidRub = Number(f.totalPaidRub ?? (f.depositPaid ? depositRub : 0));
|
|
712
|
+
const remRub = Number(f.remainingFranchiseFeeRub ?? Math.max(0, totalFeeRub - paidRub));
|
|
713
|
+
if (!isClosed) {
|
|
714
|
+
totalPaidGroup += paidRub;
|
|
715
|
+
totalRemGroup += remRub;
|
|
716
|
+
if (!f.depositPaid) pendingDepCount++;
|
|
717
|
+
else if (remRub <= 0) paidFullCount++;
|
|
718
|
+
else activeSchedCount++;
|
|
719
|
+
if (Number(f.overdueMonthsCount || 0) > 0) overdueRegionsCount++;
|
|
720
|
+
}
|
|
458
721
|
}
|
|
459
722
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
723
|
+
const subParts = [
|
|
724
|
+
fg.name,
|
|
725
|
+
fg.companyName,
|
|
726
|
+
fg.inn ? `ИНН ${fg.inn}` : '',
|
|
727
|
+
fg.phone,
|
|
728
|
+
].filter(Boolean);
|
|
729
|
+
|
|
730
|
+
const regionsRowsHtml = fg.items.map(f => {
|
|
731
|
+
const rid = f.reservationId;
|
|
732
|
+
const isClosed = f.status === 'canceled' || f.status === 'expired';
|
|
733
|
+
const depositRub = Number(f.nonRefundableDepositRub || 100_000);
|
|
734
|
+
const totalFeeRub = Number(f.franchiseFeeRub || 500_000);
|
|
735
|
+
const paidRub = Number(f.totalPaidRub ?? (f.depositPaid ? depositRub : 0));
|
|
736
|
+
const remRub = Number(f.remainingFranchiseFeeRub ?? Math.max(0, totalFeeRub - paidRub));
|
|
737
|
+
const paidMonths = Number(f.paidMonthsCount || 0);
|
|
738
|
+
const overdueMonths = Number(f.overdueMonthsCount || 0);
|
|
739
|
+
const pct = totalFeeRub > 0 ? Math.min(100, Math.round((paidRub / totalFeeRub) * 100)) : 0;
|
|
740
|
+
|
|
741
|
+
const dl = Number(f.depositDeadlineAt || ((f.createdAt || now) + 15 * 60_000));
|
|
742
|
+
const remSec = Math.max(0, Math.floor((dl - now) / 1000));
|
|
743
|
+
|
|
744
|
+
let statusAndDepositHtml = '';
|
|
745
|
+
if (isClosed) {
|
|
746
|
+
statusAndDepositHtml = `<span class="tag bad">${f.status === 'expired' ? 'Истёк таймер залога (15 мин)' : 'Отменён / Отозван'}</span>`;
|
|
747
|
+
}
|
|
748
|
+
else if (!f.depositPaid) {
|
|
749
|
+
statusAndDepositHtml = `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Ожидает залога · <b data-adm-deadline="${dl}">${fmtCountdown(remSec)}</b></span>
|
|
750
|
+
<div style="margin-top:4px"><button class="btn btn-ok btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-pay-deposit="${esc(rid)}">Зачесть залог (${rub(depositRub)})</button></div>`;
|
|
751
|
+
}
|
|
752
|
+
else if (remRub <= 0) {
|
|
753
|
+
statusAndDepositHtml = `<span class="tag ok">✓ Франшиза полностью выкуплена</span>
|
|
754
|
+
<div class="small muted">Залог ${rub(depositRub)} от ${fmtDateTime(f.paidAt)}</div>`;
|
|
755
|
+
}
|
|
756
|
+
else if (overdueMonths > 0) {
|
|
757
|
+
statusAndDepositHtml = `<span class="tag bad">Просрочка (${overdueMonths} мес.)</span>
|
|
758
|
+
<div class="small muted">Залог ✓ (${rub(depositRub)}) · от ${fmtDateTime(f.paidAt)}</div>`;
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
statusAndDepositHtml = `<span class="tag ok">Активна · Погашение по графику</span>
|
|
762
|
+
<div class="small muted">Залог ✓ (${rub(depositRub)}) · от ${fmtDateTime(f.paidAt)}</div>`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
let schedHtml = '';
|
|
766
|
+
if (isClosed) {
|
|
767
|
+
schedHtml = '<span class="small muted">Резерв закрыт</span>';
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
schedHtml = `<div><b>${paidMonths} из 12</b> ежемесячных платежей</div>
|
|
771
|
+
<div class="bar" style="width:160px;height:6px;margin-top:5px"><i style="--p:${pct}%"></i></div>
|
|
772
|
+
${f.nextInstallment && f.depositPaid ? `<div class="small muted" style="margin-top:4px">След. взнос №${f.nextInstallment.monthNumber}: ${rub(f.nextInstallment.amountRub)} до ${fmtDate(f.nextInstallment.dueDate)}</div>` : ''}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return `<tr>
|
|
776
|
+
<td>
|
|
777
|
+
<b>${esc(f.regionName)}</b> <span class="tag">${esc(f.district || f.regionCode)}</span>
|
|
778
|
+
<div class="small muted"><code>${esc(rid)}</code> · от ${fmtDateTime(f.createdAt)}</div>
|
|
779
|
+
</td>
|
|
780
|
+
<td>${statusAndDepositHtml}</td>
|
|
781
|
+
<td>${schedHtml}</td>
|
|
782
|
+
<td>
|
|
783
|
+
<div>Паушальный: <b>${rub(totalFeeRub)}</b></div>
|
|
784
|
+
<div class="small" style="color:var(--ok)">Оплачено: <b>${rub(paidRub)}</b></div>
|
|
785
|
+
<div class="small muted">Остаток: <b>${rub(remRub)}</b></div>
|
|
786
|
+
</td>
|
|
787
|
+
<td style="text-align:right">
|
|
788
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end">
|
|
789
|
+
<button class="btn btn-primary btn-sm" data-adm-sched="${esc(rid)}">График 12 мес.</button>
|
|
790
|
+
${!isClosed ? `<button class="btn btn-ghost btn-sm" style="color:var(--danger)" data-adm-cancel-franchise="${esc(rid)}">Отозвать</button>` : ''}
|
|
791
|
+
</div>
|
|
792
|
+
</td>
|
|
793
|
+
</tr>`;
|
|
794
|
+
}).join('');
|
|
795
|
+
|
|
796
|
+
return `<div class="adm-acc-card${isOpen ? ' open' : ''}" data-franchiser-card="${esc(fg.key)}">
|
|
797
|
+
<div class="adm-acc-head" data-adm-toggle-franchiser="${esc(fg.key)}">
|
|
798
|
+
<div class="adm-acc-ident">
|
|
799
|
+
<span class="adm-acc-chev">▸</span>
|
|
800
|
+
<div>
|
|
801
|
+
<div class="adm-acc-email">${esc(fg.email)}</div>
|
|
802
|
+
${subParts.length ? `<div class="adm-acc-sub">${esc(subParts.join(' · '))}</div>` : ''}
|
|
803
|
+
</div>
|
|
480
804
|
</div>
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
805
|
+
<div class="adm-acc-badges">
|
|
806
|
+
<span class="tag">Регионов: <b>${fg.items.length}</b></span>
|
|
807
|
+
${pendingDepCount > 0 ? `<span class="tag" style="background:rgba(245,158,11,.18);color:var(--warn)">Ждёт залога: ${pendingDepCount}</span>` : ''}
|
|
808
|
+
${overdueRegionsCount > 0 ? `<span class="tag bad">Просрочка: ${overdueRegionsCount}</span>` : ''}
|
|
809
|
+
${activeSchedCount > 0 && overdueRegionsCount === 0 ? `<span class="tag ok">В графике: ${activeSchedCount}</span>` : ''}
|
|
810
|
+
${paidFullCount > 0 ? `<span class="tag ok">✓ Выкуплено: ${paidFullCount}</span>` : ''}
|
|
811
|
+
<span class="small muted">Оплачено: <b style="color:var(--ok)">${rub(totalPaidGroup)}</b> · Остаток: <b>${rub(totalRemGroup)}</b></span>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
<div class="adm-acc-body">
|
|
815
|
+
<table class="sec-table">
|
|
816
|
+
<thead><tr>
|
|
817
|
+
<th>Регион РФ / ID</th>
|
|
818
|
+
<th>Текущий статус и залог</th>
|
|
819
|
+
<th>График погашения (12 мес.)</th>
|
|
820
|
+
<th>Оплачено / Остаток</th>
|
|
821
|
+
<th style="text-align:right">Управление</th>
|
|
822
|
+
</tr></thead>
|
|
823
|
+
<tbody>${regionsRowsHtml}</tbody>
|
|
824
|
+
</table>
|
|
825
|
+
</div>
|
|
826
|
+
</div>`;
|
|
827
|
+
}).join('');
|
|
484
828
|
}
|
|
485
829
|
|
|
486
830
|
/* 3. Таблица заказов и счетов */
|
|
@@ -546,41 +890,7 @@
|
|
|
546
890
|
}).join('') || '<tr><td colspan="6" class="muted">Заказов по заданному фильтру не найдено</td></tr>';
|
|
547
891
|
}
|
|
548
892
|
|
|
549
|
-
/* 4. Таблица
|
|
550
|
-
function renderUsersTable(users = []) {
|
|
551
|
-
const tbody = $('#admUsersBody');
|
|
552
|
-
if (!tbody) return;
|
|
553
|
-
const q = ($('#inpFilterUsers')?.value || '').trim().toLowerCase();
|
|
554
|
-
|
|
555
|
-
const filtered = users.filter(u => {
|
|
556
|
-
if (!q) return true;
|
|
557
|
-
const hay = [u.userId, u.email, u.companyName, u.inn, u.name].join(' ').toLowerCase();
|
|
558
|
-
return hay.includes(q);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
tbody.innerHTML = filtered.map(u => {
|
|
562
|
-
const srvCount = Array.isArray(u.servers) ? u.servers.length : (u.licenseId ? 1 : 0);
|
|
563
|
-
const frCount = Array.isArray(u.franchises) ? u.franchises.length : 0;
|
|
564
|
-
return `<tr>
|
|
565
|
-
<td><b>${esc(u.email)}</b><div class="small muted"><code>${esc(u.userId)}</code></div></td>
|
|
566
|
-
<td>
|
|
567
|
-
${u.isAdmin
|
|
568
|
-
? `<span class="tag ok">Администратор</span> <button class="btn btn-ghost btn-sm" style="margin-left:6px" data-toggle-admin="${esc(u.userId)}" data-make-admin="0">Снять права</button>`
|
|
569
|
-
: `<span class="tag">Клиент</span> <button class="btn btn-ghost btn-sm" style="margin-left:6px" data-toggle-admin="${esc(u.userId)}" data-make-admin="1">Сделать администратором</button>`}
|
|
570
|
-
</td>
|
|
571
|
-
<td><b>${esc(u.companyName || '—')}</b><div class="small muted">ИНН ${esc(u.inn || '—')}</div></td>
|
|
572
|
-
<td>Серверов: <b>${srvCount}</b> · Франшиз: <b>${frCount}</b></td>
|
|
573
|
-
<td>
|
|
574
|
-
${u.ofertaAccepted
|
|
575
|
-
? `<span class="tag ok">✓ Подписана ПЭП</span><div class="small muted"><code>${esc(u.ofertaSignatureId)}</code></div>`
|
|
576
|
-
: '<span class="tag bad">Не подписана</span>'}
|
|
577
|
-
</td>
|
|
578
|
-
<td class="small muted">${fmtDateTime(u.lastLoginAt || u.createdAt)}</td>
|
|
579
|
-
</tr>`;
|
|
580
|
-
}).join('') || '<tr><td colspan="6" class="muted">Клиентов не найдено</td></tr>';
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/* 5. Таблица тикетов SLA */
|
|
893
|
+
/* 4. Таблица тикетов SLA */
|
|
584
894
|
function renderTicketsTable(tickets = []) {
|
|
585
895
|
const tbody = $('#admTicketsBody');
|
|
586
896
|
if (!tbody) return;
|
|
@@ -671,17 +981,28 @@
|
|
|
671
981
|
}
|
|
672
982
|
|
|
673
983
|
/* ================= Действия администратора ================= */
|
|
984
|
+
function openCreateServerModal(prefillEmail = '', nextSrvNum = 1) {
|
|
985
|
+
if ($('#admCreateServerStatus')) $('#admCreateServerStatus').textContent = '';
|
|
986
|
+
if ($('#admNewSrvEmail')) $('#admNewSrvEmail').value = prefillEmail || '';
|
|
987
|
+
if ($('#admNewSrvName')) $('#admNewSrvName').value = `Сервер WORK #${Math.max(1, Number(nextSrvNum) || 1)}`;
|
|
988
|
+
if ($('#admNewSrvHwid')) $('#admNewSrvHwid').value = '';
|
|
989
|
+
if ($('#admNewSrvActivateBase')) $('#admNewSrvActivateBase').checked = true;
|
|
990
|
+
$('#admCreateServerModal')?.classList.add('open');
|
|
991
|
+
setTimeout(() => {
|
|
992
|
+
if (prefillEmail) $('#admNewSrvName')?.focus();
|
|
993
|
+
else $('#admNewSrvEmail')?.focus();
|
|
994
|
+
}, 50);
|
|
995
|
+
}
|
|
996
|
+
|
|
674
997
|
async function adminSubmitCreateServer() {
|
|
675
998
|
const st = $('#admCreateServerStatus');
|
|
676
999
|
if (st) st.textContent = '';
|
|
677
1000
|
const serverName = $('#admNewSrvName')?.value.trim() || 'Сервер WORK #1';
|
|
678
|
-
const org = $('#admNewSrvOrg')?.value.trim() || '';
|
|
679
|
-
const inn = $('#admNewSrvInn')?.value.trim() || '';
|
|
680
1001
|
const email = $('#admNewSrvEmail')?.value.trim() || '';
|
|
681
1002
|
const hwid = $('#admNewSrvHwid')?.value.trim() || '';
|
|
682
1003
|
const baseActivated = Boolean($('#admNewSrvActivateBase')?.checked);
|
|
683
1004
|
if (!email || !email.includes('@')) {
|
|
684
|
-
if (st) st.textContent = 'Укажите корректный Email
|
|
1005
|
+
if (st) st.textContent = 'Укажите корректный Email клиента.';
|
|
685
1006
|
return;
|
|
686
1007
|
}
|
|
687
1008
|
try {
|
|
@@ -691,11 +1012,10 @@
|
|
|
691
1012
|
baseActivated,
|
|
692
1013
|
ownerEmail: email,
|
|
693
1014
|
customer: {
|
|
694
|
-
name: org || email,
|
|
695
|
-
inn,
|
|
696
1015
|
email,
|
|
697
1016
|
},
|
|
698
1017
|
});
|
|
1018
|
+
expandedClients.add(email.toLowerCase());
|
|
699
1019
|
$('#admCreateServerModal')?.classList.remove('open');
|
|
700
1020
|
await refreshRegistry();
|
|
701
1021
|
}
|
|
@@ -924,13 +1244,147 @@
|
|
|
924
1244
|
}
|
|
925
1245
|
}
|
|
926
1246
|
|
|
1247
|
+
function showFeedback(elId, msg, isErr = false) {
|
|
1248
|
+
const el = document.getElementById(elId);
|
|
1249
|
+
if (!el) return;
|
|
1250
|
+
el.textContent = msg;
|
|
1251
|
+
el.style.color = isErr ? 'var(--danger)' : 'var(--ok)';
|
|
1252
|
+
setTimeout(() => {
|
|
1253
|
+
if (el.textContent === msg) el.textContent = '';
|
|
1254
|
+
}, 4500);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function applySmtpPreset(preset = '') {
|
|
1258
|
+
const map = {
|
|
1259
|
+
yandex: { host: 'smtp.yandex.ru', port: 465, secure: true },
|
|
1260
|
+
gmail: { host: 'smtp.gmail.com', port: 465, secure: true },
|
|
1261
|
+
mailru: { host: 'smtp.mail.ru', port: 465, secure: true },
|
|
1262
|
+
};
|
|
1263
|
+
const p = map[preset];
|
|
1264
|
+
if (!p) return;
|
|
1265
|
+
if ($('#admSmtpHost')) $('#admSmtpHost').value = p.host;
|
|
1266
|
+
if ($('#admSmtpPort')) $('#admSmtpPort').value = p.port;
|
|
1267
|
+
if ($('#admSmtpSecure')) $('#admSmtpSecure').checked = p.secure;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function saveSmtpSettings() {
|
|
1271
|
+
try {
|
|
1272
|
+
const res = await api('/api/admin/settings/smtp', {
|
|
1273
|
+
host: $('#admSmtpHost')?.value.trim() || '',
|
|
1274
|
+
port: Number($('#admSmtpPort')?.value || 465),
|
|
1275
|
+
secure: Boolean($('#admSmtpSecure')?.checked),
|
|
1276
|
+
user: $('#admSmtpUser')?.value.trim() || '',
|
|
1277
|
+
pass: $('#admSmtpPass')?.value || '',
|
|
1278
|
+
fromEmail: $('#admSmtpFrom')?.value.trim() || '',
|
|
1279
|
+
senderName: $('#admSmtpSenderName')?.value.trim() || 'ODANT-WORK',
|
|
1280
|
+
});
|
|
1281
|
+
if (lastAdminData && res.settings) lastAdminData.settings = res.settings;
|
|
1282
|
+
renderPortalSettings(res.settings || {});
|
|
1283
|
+
showFeedback('admSmtpFeedback', '✓ Настройки SMTP сохранены');
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
showFeedback('admSmtpFeedback', err.message, true);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async function testSmtpSettings() {
|
|
1291
|
+
try {
|
|
1292
|
+
await saveSmtpSettings();
|
|
1293
|
+
const to = $('#admProfileEmail')?.textContent?.includes('@')
|
|
1294
|
+
? $('#admProfileEmail').textContent.trim()
|
|
1295
|
+
: ($('#admSmtpFrom')?.value.trim() || $('#admSmtpUser')?.value.trim() || '');
|
|
1296
|
+
const res = await api('/api/admin/settings/smtp/test', { to });
|
|
1297
|
+
showFeedback('admSmtpFeedback', `✓ ${res.message || 'Тестовое письмо отправлено'}`);
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
showFeedback('admSmtpFeedback', err.message, true);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async function saveYooKassaSettings() {
|
|
1305
|
+
try {
|
|
1306
|
+
const res = await api('/api/admin/settings/yookassa', {
|
|
1307
|
+
shopId: $('#admYkShopId')?.value.trim() || '',
|
|
1308
|
+
secretKey: $('#admYkSecretKey')?.value.trim() || '',
|
|
1309
|
+
sendReceipt: Boolean($('#admYkSendReceipt')?.checked),
|
|
1310
|
+
});
|
|
1311
|
+
if (lastAdminData && res.settings) lastAdminData.settings = res.settings;
|
|
1312
|
+
renderPortalSettings(res.settings || {});
|
|
1313
|
+
showFeedback('admYkFeedback', '✓ Настройки ЮKassa сохранены');
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
showFeedback('admYkFeedback', err.message, true);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async function saveBotsSettings() {
|
|
1321
|
+
try {
|
|
1322
|
+
const res = await api('/api/admin/settings/bots', {
|
|
1323
|
+
telegramToken: $('#admBotTgToken')?.value.trim() || '',
|
|
1324
|
+
telegramUsername: $('#admBotTgUsername')?.value.trim() || '',
|
|
1325
|
+
maxToken: $('#admBotMaxToken')?.value.trim() || '',
|
|
1326
|
+
maxUsername: $('#admBotMaxUsername')?.value.trim() || '',
|
|
1327
|
+
});
|
|
1328
|
+
if (lastAdminData && res.settings) lastAdminData.settings = res.settings;
|
|
1329
|
+
renderPortalSettings(res.settings || {});
|
|
1330
|
+
showFeedback('admBotsFeedback', '✓ Боты платформы сохранены');
|
|
1331
|
+
}
|
|
1332
|
+
catch (err) {
|
|
1333
|
+
showFeedback('admBotsFeedback', err.message, true);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function saveLegalSettings() {
|
|
1338
|
+
try {
|
|
1339
|
+
const res = await api('/api/admin/settings/legal', {
|
|
1340
|
+
companyName: $('#admLegalCompany')?.value.trim() || '',
|
|
1341
|
+
authorName: $('#admLegalAuthor')?.value.trim() || '',
|
|
1342
|
+
legalStatus: $('#admLegalStatus')?.value.trim() || '',
|
|
1343
|
+
inn: $('#admLegalInn')?.value.trim() || '',
|
|
1344
|
+
ogrn: $('#admLegalOgrn')?.value.trim() || '',
|
|
1345
|
+
supportEmail: $('#admLegalEmail')?.value.trim() || '',
|
|
1346
|
+
phone: $('#admLegalPhone')?.value.trim() || '',
|
|
1347
|
+
address: $('#admLegalAddress')?.value.trim() || '',
|
|
1348
|
+
});
|
|
1349
|
+
if (lastAdminData && res.settings) lastAdminData.settings = res.settings;
|
|
1350
|
+
renderPortalSettings(res.settings || {});
|
|
1351
|
+
showFeedback('admLegalFeedback', '✓ Юридические реквизиты оферты сохранены');
|
|
1352
|
+
}
|
|
1353
|
+
catch (err) {
|
|
1354
|
+
showFeedback('admLegalFeedback', err.message, true);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
927
1358
|
/* ================= Привязка событий ================= */
|
|
928
1359
|
function bind() {
|
|
929
1360
|
document.addEventListener('click', (e) => {
|
|
930
|
-
const el = e.target.closest('[data-action],[data-adm-tab],[data-cert],[data-adm-dl-licfile],[data-adm-base],[data-adm-reset-hwid],[data-adm-open-grant],[data-adm-copy-tok],[data-adm-sched],[data-adm-pay-deposit],[data-adm-confirm-inst],[data-adm-cancel-franchise],[data-pay-order],[data-cancel-order],[data-reply-ticket],[data-close-ticket],[data-toggle-admin]');
|
|
1361
|
+
const el = e.target.closest('[data-action],[data-adm-tab],[data-smtp-preset],[data-adm-add-srv-email],[data-adm-toggle-client],[data-adm-toggle-franchiser],[data-cert],[data-adm-dl-licfile],[data-adm-base],[data-adm-reset-hwid],[data-adm-open-grant],[data-adm-copy-tok],[data-adm-sched],[data-adm-pay-deposit],[data-adm-confirm-inst],[data-adm-cancel-franchise],[data-pay-order],[data-cancel-order],[data-reply-ticket],[data-close-ticket],[data-toggle-admin]');
|
|
931
1362
|
if (!el) return;
|
|
932
1363
|
|
|
933
1364
|
if (el.dataset.admTab) { setTab(el.dataset.admTab); return; }
|
|
1365
|
+
if (el.dataset.smtpPreset) { applySmtpPreset(el.dataset.smtpPreset); return; }
|
|
1366
|
+
if (el.dataset.admAddSrvEmail !== undefined) {
|
|
1367
|
+
openCreateServerModal(el.dataset.admAddSrvEmail, el.dataset.admNextNum || 1);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (el.dataset.admToggleClient) {
|
|
1371
|
+
const key = el.dataset.admToggleClient;
|
|
1372
|
+
const card = el.closest('.adm-acc-card');
|
|
1373
|
+
const nowOpen = !card?.classList.contains('open');
|
|
1374
|
+
if (nowOpen) expandedClients.add(key);
|
|
1375
|
+
else expandedClients.delete(key);
|
|
1376
|
+
card?.classList.toggle('open', nowOpen);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (el.dataset.admToggleFranchiser) {
|
|
1380
|
+
const key = el.dataset.admToggleFranchiser;
|
|
1381
|
+
const card = el.closest('.adm-acc-card');
|
|
1382
|
+
const nowOpen = !card?.classList.contains('open');
|
|
1383
|
+
if (nowOpen) expandedFranchisers.add(key);
|
|
1384
|
+
else expandedFranchisers.delete(key);
|
|
1385
|
+
card?.classList.toggle('open', nowOpen);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
934
1388
|
if (el.dataset.cert) { openCertModal(el.dataset.cert); return; }
|
|
935
1389
|
if (el.dataset.admDlLicfile !== undefined) { downloadCertLicFile(el.dataset.admDlLicfile); return; }
|
|
936
1390
|
if (el.dataset.admBase) {
|
|
@@ -987,7 +1441,7 @@
|
|
|
987
1441
|
case 'close-adm-profile': $('#admProfileModal')?.classList.remove('open'); break;
|
|
988
1442
|
case 'admin-logout': logoutAdmin(); break;
|
|
989
1443
|
case 'registry': refreshRegistry(); break;
|
|
990
|
-
case 'open-create-server-modal':
|
|
1444
|
+
case 'open-create-server-modal': openCreateServerModal('', 1); break;
|
|
991
1445
|
case 'close-create-server-modal': $('#admCreateServerModal')?.classList.remove('open'); break;
|
|
992
1446
|
case 'submit-create-server': adminSubmitCreateServer(); break;
|
|
993
1447
|
case 'close-grant-modal': $('#admGrantSeatsModal')?.classList.remove('open'); break;
|
|
@@ -1001,6 +1455,11 @@
|
|
|
1001
1455
|
case 'admin-export-backup': exportAdminBackup(); break;
|
|
1002
1456
|
case 'admin-import-backup': $('#inpAdmBackupFile')?.click(); break;
|
|
1003
1457
|
case 'admin-recover-npx': recoverAdminFromNpx(); break;
|
|
1458
|
+
case 'save-smtp-settings': saveSmtpSettings(); break;
|
|
1459
|
+
case 'test-smtp-settings': testSmtpSettings(); break;
|
|
1460
|
+
case 'save-yookassa-settings': saveYooKassaSettings(); break;
|
|
1461
|
+
case 'save-bots-settings': saveBotsSettings(); break;
|
|
1462
|
+
case 'save-legal-settings': saveLegalSettings(); break;
|
|
1004
1463
|
}
|
|
1005
1464
|
});
|
|
1006
1465
|
|
|
@@ -1018,7 +1477,7 @@
|
|
|
1018
1477
|
}
|
|
1019
1478
|
});
|
|
1020
1479
|
|
|
1021
|
-
['
|
|
1480
|
+
['inpFilterClients', 'selFilterClients', 'inpFilterFranchisers', 'selFilterFranchisers', 'inpFilterOrders', 'selFilterOrders', 'selFilterTickets'].forEach(id => {
|
|
1022
1481
|
const el = document.getElementById(id);
|
|
1023
1482
|
if (!el) return;
|
|
1024
1483
|
el.addEventListener('input', () => lastAdminData && renderAdminPanel(lastAdminData));
|
|
@@ -1036,7 +1495,7 @@
|
|
|
1036
1495
|
function start() {
|
|
1037
1496
|
applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
|
|
1038
1497
|
bind();
|
|
1039
|
-
setTab('
|
|
1498
|
+
setTab('clients');
|
|
1040
1499
|
if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
|
|
1041
1500
|
checkAdminSession();
|
|
1042
1501
|
}
|