work-license-server 1.0.19 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/landing.js +440 -40
- package/package.json +1 -1
- package/src/catalog.js +1 -1
- package/src/landing.js +73 -0
- package/src/server.js +33 -1
- package/src/store.js +201 -32
package/assets/landing.js
CHANGED
|
@@ -397,12 +397,92 @@
|
|
|
397
397
|
|
|
398
398
|
let franchiseCountdownTimer = null;
|
|
399
399
|
let activeCabinetTab = 'servers';
|
|
400
|
+
const openServerAddonBuilders = new Map();
|
|
401
|
+
const openInvoiceOrderIds = new Set();
|
|
402
|
+
|
|
403
|
+
function cleanCompanyName(val = '') {
|
|
404
|
+
const s = String(val || '').trim();
|
|
405
|
+
if (!s || s.includes('@') || s === 'Организация WORK' || s === 'Клиент ODANT-WORK') return '';
|
|
406
|
+
return s;
|
|
407
|
+
}
|
|
400
408
|
|
|
401
409
|
function switchCabinetTab(tab = 'servers') {
|
|
402
|
-
activeCabinetTab = tab === 'franchises'
|
|
410
|
+
activeCabinetTab = (tab === 'franchises' || tab === 'requisites') ? tab : 'servers';
|
|
403
411
|
$$('[data-cab-tab]').forEach(b => b.classList.toggle('active', b.dataset.cabTab === activeCabinetTab));
|
|
404
412
|
if ($('#cabSecServers')) $('#cabSecServers').style.display = activeCabinetTab === 'servers' ? '' : 'none';
|
|
405
413
|
if ($('#cabSecFranchises')) $('#cabSecFranchises').style.display = activeCabinetTab === 'franchises' ? '' : 'none';
|
|
414
|
+
if ($('#cabSecRequisites')) $('#cabSecRequisites').style.display = activeCabinetTab === 'requisites' ? '' : 'none';
|
|
415
|
+
if (activeCabinetTab === 'requisites') renderCabinetRequisites();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderCabinetRequisites() {
|
|
419
|
+
const u = currentUser || {};
|
|
420
|
+
const comp = cleanCompanyName(u.companyName);
|
|
421
|
+
const hasReq = Boolean(comp && u.inn);
|
|
422
|
+
if ($('#navCabRequisites')) {
|
|
423
|
+
$('#navCabRequisites').textContent = hasReq ? `Реквизиты организации (✓)` : `Реквизиты организации`;
|
|
424
|
+
}
|
|
425
|
+
if ($('#reqCompanyName')) $('#reqCompanyName').value = comp;
|
|
426
|
+
if ($('#reqInn')) $('#reqInn').value = u.inn || '';
|
|
427
|
+
if ($('#reqKpp')) $('#reqKpp').value = u.kpp || '';
|
|
428
|
+
if ($('#reqLegalAddress')) $('#reqLegalAddress').value = u.legalAddress || '';
|
|
429
|
+
if ($('#reqSignerPosition')) $('#reqSignerPosition').value = u.signerPosition || u.position || '';
|
|
430
|
+
if ($('#reqSignerName')) $('#reqSignerName').value = u.signerName || '';
|
|
431
|
+
if ($('#reqBankName')) $('#reqBankName').value = u.bankName || '';
|
|
432
|
+
if ($('#reqBik')) $('#reqBik').value = u.bik || '';
|
|
433
|
+
if ($('#reqPhone')) $('#reqPhone').value = u.phone || '';
|
|
434
|
+
if ($('#reqBankAccount')) $('#reqBankAccount').value = u.bankAccount || '';
|
|
435
|
+
if ($('#reqCorrAccount')) $('#reqCorrAccount').value = u.corrAccount || '';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function saveCabinetRequisites() {
|
|
439
|
+
if (!currentUser?.sessionKey) return;
|
|
440
|
+
const st = $('#reqSaveStatus');
|
|
441
|
+
const companyName = $('#reqCompanyName')?.value.trim() || '';
|
|
442
|
+
const inn = $('#reqInn')?.value.trim() || '';
|
|
443
|
+
if (!companyName || !inn) {
|
|
444
|
+
if (st) {
|
|
445
|
+
st.style.color = 'var(--danger)';
|
|
446
|
+
st.textContent = 'Укажите как минимум Наименование организации и ИНН.';
|
|
447
|
+
}
|
|
448
|
+
(!companyName ? $('#reqCompanyName') : $('#reqInn'))?.focus();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (st) {
|
|
452
|
+
st.style.color = 'var(--muted)';
|
|
453
|
+
st.textContent = 'Сохранение реквизитов…';
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const res = await api('/api/cabinet/profile', {
|
|
457
|
+
sessionKey: currentUser.sessionKey,
|
|
458
|
+
companyName,
|
|
459
|
+
inn,
|
|
460
|
+
kpp: $('#reqKpp')?.value.trim() || '',
|
|
461
|
+
legalAddress: $('#reqLegalAddress')?.value.trim() || '',
|
|
462
|
+
signerPosition: $('#reqSignerPosition')?.value.trim() || '',
|
|
463
|
+
signerName: $('#reqSignerName')?.value.trim() || '',
|
|
464
|
+
bankName: $('#reqBankName')?.value.trim() || '',
|
|
465
|
+
bik: $('#reqBik')?.value.trim() || '',
|
|
466
|
+
phone: $('#reqPhone')?.value.trim() || '',
|
|
467
|
+
bankAccount: $('#reqBankAccount')?.value.trim() || '',
|
|
468
|
+
corrAccount: $('#reqCorrAccount')?.value.trim() || '',
|
|
469
|
+
});
|
|
470
|
+
if (res.user) {
|
|
471
|
+
currentUser = { ...res.user, sessionKey: res.user.sessionKey || currentUser.sessionKey };
|
|
472
|
+
persistSessionInBrowser(currentUser.sessionKey, currentUser.email, currentUser.isAdmin, currentUser);
|
|
473
|
+
onAuthenticated();
|
|
474
|
+
}
|
|
475
|
+
if (st) {
|
|
476
|
+
st.style.color = 'var(--ok)';
|
|
477
|
+
st.textContent = '✓ Реквизиты организации сохранены в Личном кабинете и применены к вашим счетам.';
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
if (st) {
|
|
482
|
+
st.style.color = 'var(--danger)';
|
|
483
|
+
st.textContent = err.message;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
406
486
|
}
|
|
407
487
|
|
|
408
488
|
function onAuthenticated() {
|
|
@@ -415,17 +495,19 @@
|
|
|
415
495
|
sessionStorage.setItem('odant_work_admin_token', currentUser.sessionKey);
|
|
416
496
|
} catch { /* ignore */ }
|
|
417
497
|
}
|
|
418
|
-
|
|
498
|
+
const comp = cleanCompanyName(currentUser.companyName);
|
|
499
|
+
if ($('#btnAuthText')) $('#btnAuthText').textContent = comp || currentUser.email;
|
|
419
500
|
if ($('#authEmail')) $('#authEmail').value = currentUser.email || '';
|
|
420
501
|
if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = Boolean(currentUser.ofertaAccepted);
|
|
421
502
|
if ($('#userBadgeBox')) {
|
|
422
|
-
const orgInfo =
|
|
503
|
+
const orgInfo = comp ? `${esc(comp)}${currentUser.inn ? ' (ИНН ' + esc(currentUser.inn) + ')' : ''} · ` : '';
|
|
423
504
|
const adminBadge = currentUser.isAdmin ? `<span class="tag ok" style="margin-right:6px">Администратор</span> <a href="/admin" style="font-weight:700;margin-right:8px">Открыть Админ-панель →</a> · ` : '';
|
|
424
505
|
$('#userBadgeBox').innerHTML = `${adminBadge}${orgInfo}Аккаунт: <b>${esc(currentUser.email)}</b> · <a href="#" data-action="logout" style="color:var(--muted)">Выйти</a>`;
|
|
425
506
|
}
|
|
426
507
|
syncClientServerPicker();
|
|
427
508
|
renderCabinetServers();
|
|
428
509
|
renderCabinetFranchises();
|
|
510
|
+
renderCabinetRequisites();
|
|
429
511
|
moveIndicator();
|
|
430
512
|
}
|
|
431
513
|
|
|
@@ -482,6 +564,116 @@
|
|
|
482
564
|
}
|
|
483
565
|
}
|
|
484
566
|
|
|
567
|
+
function formatOrderSummaryHtml(o = {}) {
|
|
568
|
+
const parts = [];
|
|
569
|
+
if (o.includeBasePak) {
|
|
570
|
+
parts.push(`<b>Комплекс ODANT-WORK с сервером</b> (10 вечных мест + ИИ)`);
|
|
571
|
+
}
|
|
572
|
+
else if (o.includeBaseLicenseOnly) {
|
|
573
|
+
parts.push(`<b>Базовая лицензия ODANT-WORK</b> (10 вечных мест + ИИ)`);
|
|
574
|
+
}
|
|
575
|
+
const seats = Number(o.addonSeats || 0);
|
|
576
|
+
if (seats > 0) {
|
|
577
|
+
const years = Number(o.years || 1);
|
|
578
|
+
const tierObj = tierById(o.tier || 'business');
|
|
579
|
+
const tierName = o.tierName || tierObj?.name || 'Business';
|
|
580
|
+
const disc = volumeDiscount(seats).discountPercent;
|
|
581
|
+
const gross = seats * (tierObj?.pricePerUserYearRub || 15_000) * years;
|
|
582
|
+
const net = (!o.includeBasePak && !o.includeBaseLicenseOnly && o.amountRub)
|
|
583
|
+
? Number(o.amountRub)
|
|
584
|
+
: Math.round(gross * (1 - disc / 100));
|
|
585
|
+
const perSeat = Math.round(net / seats);
|
|
586
|
+
const discStr = disc > 0 ? `, скидка −${disc}%` : '';
|
|
587
|
+
parts.push(`Дополнительные рабочие места: <b>${seats} мест</b> × <b>${years} г.</b> · тариф <b>${esc(tierName)}</b> (${rub(perSeat)}/место${discStr})`);
|
|
588
|
+
}
|
|
589
|
+
if (!parts.length && Array.isArray(o.items) && o.items.length) {
|
|
590
|
+
for (const it of o.items) {
|
|
591
|
+
const q = Number(it.quantity || 1);
|
|
592
|
+
const hasQtyInTitle = /\b\d+\s*(раб\.?\s*мест|мест|шт)/i.test(String(it.title || ''));
|
|
593
|
+
parts.push(`${(!hasQtyInTitle && q > 1) ? `<b>${q} мест</b> · ` : ''}${esc(it.title)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return parts.join(' + ') || esc(o.tierName || o.orderId);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function calcInlineAddonQuote(cfg = {}) {
|
|
600
|
+
const seats = Math.max(1, Math.min(2000, Math.floor(Number(cfg.seats) || 10)));
|
|
601
|
+
const years = Math.max(1, Math.min(5, Math.floor(Number(cfg.years) || 1)));
|
|
602
|
+
const tier = tierById(cfg.tier || 'business');
|
|
603
|
+
const disc = volumeDiscount(seats).discountPercent;
|
|
604
|
+
const gross = seats * tier.pricePerUserYearRub * years;
|
|
605
|
+
const saved = Math.round(gross * disc / 100);
|
|
606
|
+
const net = gross - saved;
|
|
607
|
+
const perSeat = Math.round(net / seats);
|
|
608
|
+
return { seats, years, tier, disc, gross, saved, net, perSeat };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function renderServerAddonBuilderHtml(lid, srvName) {
|
|
612
|
+
const cfg = openServerAddonBuilders.get(lid);
|
|
613
|
+
if (!cfg) return '';
|
|
614
|
+
const q = calcInlineAddonQuote(cfg);
|
|
615
|
+
const presets = [5, 10, 25, 50, 100];
|
|
616
|
+
const tiers = D.tiers || [
|
|
617
|
+
{ id: 'start', name: 'Start', pricePerUserYearRub: 10_000 },
|
|
618
|
+
{ id: 'business', name: 'Business', pricePerUserYearRub: 15_000 },
|
|
619
|
+
{ id: 'enterprise', name: 'Enterprise', pricePerUserYearRub: 20_000 },
|
|
620
|
+
];
|
|
621
|
+
const yearOpts = [1, 2, 3, 5];
|
|
622
|
+
|
|
623
|
+
return `<div style="margin-top:12px;padding:14px 16px;border-radius:10px;border:1px solid var(--brand);background:color-mix(in srgb, var(--brand) 6%, var(--panel-solid))">
|
|
624
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:12px">
|
|
625
|
+
<div style="font-weight:700;font-size:14px">Докупка дополнительных рабочих мест для «${esc(srvName)}»</div>
|
|
626
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:12px" data-close-addon-builder="${esc(lid)}">✕ Закрыть</button>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;align-items:end">
|
|
630
|
+
<div>
|
|
631
|
+
<div class="small muted" style="margin-bottom:6px">1. Количество дополнительных мест:</div>
|
|
632
|
+
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
633
|
+
<input type="number" min="1" max="2000" value="${q.seats}" data-addon-seats-input="${esc(lid)}" style="width:90px;padding:6px 10px;font-weight:700;font-size:14px">
|
|
634
|
+
${presets.map(p => {
|
|
635
|
+
const d = volumeDiscount(p).discountPercent;
|
|
636
|
+
const active = q.seats === p;
|
|
637
|
+
return `<button type="button" class="btn ${active ? 'btn-primary' : 'btn-ghost'} btn-sm" style="padding:5px 9px;font-size:12px" data-addon-preset="${esc(lid)}" data-seats="${p}">${p}${d ? ` (−${d}%)` : ''}</button>`;
|
|
638
|
+
}).join('')}
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div>
|
|
643
|
+
<div class="small muted" style="margin-bottom:6px">2. Уровень поддержки (SLA):</div>
|
|
644
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
645
|
+
${tiers.map(t => {
|
|
646
|
+
const active = q.tier.id === t.id;
|
|
647
|
+
return `<button type="button" class="btn ${active ? 'btn-primary' : 'btn-ghost'} btn-sm" style="padding:5px 10px;font-size:12px" data-addon-tier="${esc(lid)}" data-tier="${esc(t.id)}">${esc(t.name)} · ${rub(t.pricePerUserYearRub)}/г.</button>`;
|
|
648
|
+
}).join('')}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
<div>
|
|
653
|
+
<div class="small muted" style="margin-bottom:6px">3. Срок действия лицензий:</div>
|
|
654
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
655
|
+
${yearOpts.map(y => {
|
|
656
|
+
const active = q.years === y;
|
|
657
|
+
const lbl = y === 1 ? '1 год' : (y < 5 ? `${y} года` : '5 лет');
|
|
658
|
+
return `<button type="button" class="btn ${active ? 'btn-primary' : 'btn-ghost'} btn-sm" style="padding:5px 10px;font-size:12px" data-addon-years="${esc(lid)}" data-years="${y}">${lbl}</button>`;
|
|
659
|
+
}).join('')}
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
</div>
|
|
663
|
+
|
|
664
|
+
<div id="addonQuoteSummary-${esc(lid)}" style="margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--field);border:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
|
|
665
|
+
<div class="small">
|
|
666
|
+
Выбрано: <b>${q.seats} раб. мест</b> × <b>${q.years} г.</b> · Тариф <b>${esc(q.tier.name)}</b> · Цена за место: <b>${rub(q.perSeat)}</b>${q.disc > 0 ? ` <span class="tag ok" style="margin-left:4px">скидка −${q.disc}% (−${rub(q.saved)})</span>` : ''}
|
|
667
|
+
</div>
|
|
668
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
669
|
+
<span style="font-size:15px;font-weight:800;margin-right:4px">Итого: ${rub(q.net)}</span>
|
|
670
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12.5px" data-submit-addon="${esc(lid)}" data-pay-mode="yookassa">Оплатить ЮKassa</button>
|
|
671
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:6px 12px;font-size:12.5px" data-submit-addon="${esc(lid)}" data-pay-mode="invoice">Счёт юрлицу</button>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>`;
|
|
675
|
+
}
|
|
676
|
+
|
|
485
677
|
function renderCabinetServers() {
|
|
486
678
|
const box = $('#cabinetServersRegistry');
|
|
487
679
|
if (!box) return;
|
|
@@ -495,6 +687,11 @@
|
|
|
495
687
|
return;
|
|
496
688
|
}
|
|
497
689
|
|
|
690
|
+
const savedCompany = cleanCompanyName(currentUser?.companyName);
|
|
691
|
+
const savedInn = currentUser?.inn || '';
|
|
692
|
+
const savedKpp = currentUser?.kpp || '';
|
|
693
|
+
const savedAddr = currentUser?.legalAddress || '';
|
|
694
|
+
|
|
498
695
|
box.innerHTML = servers.map((s, idx) => {
|
|
499
696
|
const lid = s.licenseId;
|
|
500
697
|
const name = s.serverName || `Сервер WORK #${idx + 1}`;
|
|
@@ -516,8 +713,8 @@
|
|
|
516
713
|
const canDelete = !s.baseActivated && !activeAddons.length && !paidOrders.length;
|
|
517
714
|
|
|
518
715
|
const baseActionsHtml = !s.baseActivated
|
|
519
|
-
? `<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>
|
|
520
|
-
<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>`
|
|
716
|
+
? `<button type="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>
|
|
717
|
+
<button type="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>`
|
|
521
718
|
: '';
|
|
522
719
|
|
|
523
720
|
const addonsSummaryHtml = activeAddons.length
|
|
@@ -530,27 +727,59 @@
|
|
|
530
727
|
const filesHtml = files.length
|
|
531
728
|
? `<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
532
729
|
<span class="small muted">Файлы лицензий (<code>#system</code>):</span>
|
|
533
|
-
${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('')}
|
|
730
|
+
${files.map((f, fIdx) => `<button type="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('')}
|
|
534
731
|
</div>`
|
|
535
732
|
: '';
|
|
536
733
|
|
|
734
|
+
const addonBuilderHtml = renderServerAddonBuilderHtml(lid, name);
|
|
735
|
+
|
|
537
736
|
const pendingOrdersHtml = pendingOrders.length
|
|
538
|
-
? `<div style="margin-top:10px;
|
|
737
|
+
? `<div style="margin-top:10px;display:flex;flex-direction:column;gap:8px">
|
|
539
738
|
${pendingOrders.map(o => {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
739
|
+
const summaryHtml = formatOrderSummaryHtml(o);
|
|
740
|
+
const ordComp = cleanCompanyName(o.customer?.name) || savedCompany;
|
|
741
|
+
const ordInn = o.customer?.inn || savedInn;
|
|
742
|
+
const ordKpp = o.customer?.kpp || savedKpp;
|
|
743
|
+
const ordAddr = o.customer?.legalAddress || savedAddr;
|
|
744
|
+
const isInvOpen = openInvoiceOrderIds.has(o.orderId) || (o.paymentMethod === 'invoice' && (!ordComp || !ordInn));
|
|
745
|
+
const hasReadyInvoice = Boolean(o.invoiceNumber && ordComp && ordInn);
|
|
746
|
+
|
|
747
|
+
return `<div style="padding:12px 14px;border-radius:10px;border:1px solid var(--line-strong);background:color-mix(in srgb, var(--warn) 6%, transparent)">
|
|
748
|
+
<div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
|
|
749
|
+
<div class="small"><b>Ожидает оплаты:</b> ${summaryHtml} · <b style="font-size:14px">${rub(o.amountRub)}</b></div>
|
|
750
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
|
|
751
|
+
<button type="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>
|
|
752
|
+
<button type="button" class="btn ${isInvOpen ? 'btn-primary' : 'btn-ghost'} btn-sm" style="padding:5px 10px;font-size:12px" data-invoice-toggle="${esc(o.orderId)}">Счёт юрлицу</button>
|
|
753
|
+
${hasReadyInvoice ? `<a class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;text-decoration:none" href="/api/orders/${encodeURIComponent(o.orderId)}/invoice" target="_blank" rel="noopener">📄 Счёт № ${esc(o.invoiceNumber)} ↗</a>` : ''}
|
|
754
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;color:var(--danger)" data-cancel-order="${esc(o.orderId)}">Отменить</button>
|
|
755
|
+
</div>
|
|
547
756
|
</div>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
757
|
+
|
|
758
|
+
<div id="invBox-${esc(o.orderId)}" style="display:${isInvOpen ? '' : 'none'};margin-top:12px;padding-top:12px;border-top:1px solid var(--line)">
|
|
759
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px">
|
|
760
|
+
<span class="small"><b>Реквизиты организации (Лицензиата) для выставления счёта-договора:</b> <span class="muted">(автоматически сохраняются в ЛК)</span></span>
|
|
761
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px" data-cab-tab="requisites">Полные банковские реквизиты →</button>
|
|
762
|
+
</div>
|
|
763
|
+
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 2fr auto;gap:8px;align-items:end">
|
|
764
|
+
<div class="field" style="margin:0">
|
|
765
|
+
<label style="font-size:11.5px">Организация / ИП *</label>
|
|
766
|
+
<input type="text" id="invCompany-${esc(o.orderId)}" value="${esc(ordComp)}" placeholder="ООО «Маслоторг»" style="padding:6px 10px;font-size:13px">
|
|
767
|
+
</div>
|
|
768
|
+
<div class="field" style="margin:0">
|
|
769
|
+
<label style="font-size:11.5px">ИНН *</label>
|
|
770
|
+
<input type="text" id="invInn-${esc(o.orderId)}" value="${esc(ordInn)}" placeholder="10 или 12 цифр" maxlength="12" style="padding:6px 10px;font-size:13px">
|
|
771
|
+
</div>
|
|
772
|
+
<div class="field" style="margin:0">
|
|
773
|
+
<label style="font-size:11.5px">КПП</label>
|
|
774
|
+
<input type="text" id="invKpp-${esc(o.orderId)}" value="${esc(ordKpp)}" placeholder="9 цифр" maxlength="9" style="padding:6px 10px;font-size:13px">
|
|
775
|
+
</div>
|
|
776
|
+
<div class="field" style="margin:0">
|
|
777
|
+
<label style="font-size:11.5px">Юридический адрес</label>
|
|
778
|
+
<input type="text" id="invAddr-${esc(o.orderId)}" value="${esc(ordAddr)}" placeholder="Город, улица, дом" style="padding:6px 10px;font-size:13px">
|
|
779
|
+
</div>
|
|
780
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:8px 14px;font-size:12.5px;height:36px;white-space:nowrap" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Открыть счёт-договор ↗</button>
|
|
781
|
+
</div>
|
|
782
|
+
<div id="invErr-${esc(o.orderId)}" class="small" style="color:var(--danger);margin-top:6px;display:none"></div>
|
|
554
783
|
</div>
|
|
555
784
|
</div>`;
|
|
556
785
|
}).join('')}
|
|
@@ -566,8 +795,8 @@
|
|
|
566
795
|
</div>
|
|
567
796
|
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
|
568
797
|
<span class="small muted" style="margin-right:4px">Сессий: <b>${s.totalSeats}</b></span>
|
|
569
|
-
<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>
|
|
570
|
-
${canDelete ? `<button class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;color:var(--danger)" data-delete-server="${esc(lid)}">Удалить</button>` : ''}
|
|
798
|
+
<button type="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>
|
|
799
|
+
${canDelete ? `<button type="button" class="btn btn-ghost btn-sm" style="padding:5px 10px;font-size:12px;color:var(--danger)" data-delete-server="${esc(lid)}">Удалить</button>` : ''}
|
|
571
800
|
</div>
|
|
572
801
|
</div>
|
|
573
802
|
|
|
@@ -575,8 +804,8 @@
|
|
|
575
804
|
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
576
805
|
<span class="small muted" style="min-width:115px">Токен привязки:</span>
|
|
577
806
|
<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">
|
|
578
|
-
<button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12px" data-copy-server-token="${esc(lid)}">Скопировать токен</button>
|
|
579
|
-
<button class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px" data-regen-server-token="${esc(lid)}">Перевыпустить</button>
|
|
807
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12px" data-copy-server-token="${esc(lid)}">Скопировать токен</button>
|
|
808
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:6px 10px;font-size:12px" data-regen-server-token="${esc(lid)}">Перевыпустить</button>
|
|
580
809
|
<span class="small" id="srvTokStatus-${esc(lid)}" style="color:var(--ok)"></span>
|
|
581
810
|
</div>
|
|
582
811
|
|
|
@@ -588,10 +817,11 @@
|
|
|
588
817
|
</div>
|
|
589
818
|
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
590
819
|
${baseActionsHtml}
|
|
591
|
-
<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>
|
|
820
|
+
<button type="button" class="btn ${openServerAddonBuilders.has(lid) ? 'btn-primary' : 'btn-ghost'} btn-sm" style="padding:5px 10px;font-size:12px" data-buy-for-server="${esc(lid)}" data-buy-variant="addons_only">+ Докупить рабочие места</button>
|
|
592
821
|
</div>
|
|
593
822
|
</div>
|
|
594
823
|
|
|
824
|
+
${addonBuilderHtml}
|
|
595
825
|
${filesHtml}
|
|
596
826
|
${pendingOrdersHtml}
|
|
597
827
|
</div>`;
|
|
@@ -865,10 +1095,13 @@
|
|
|
865
1095
|
|
|
866
1096
|
async function buyForSpecificServer(licenseId, variant = 'license_only') {
|
|
867
1097
|
if (variant === 'addons_only') {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1098
|
+
if (openServerAddonBuilders.has(licenseId)) {
|
|
1099
|
+
openServerAddonBuilders.delete(licenseId);
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
openServerAddonBuilders.set(licenseId, { seats: 10, tier: 'business', years: 1 });
|
|
1103
|
+
}
|
|
1104
|
+
renderCabinetServers();
|
|
872
1105
|
return;
|
|
873
1106
|
}
|
|
874
1107
|
try {
|
|
@@ -883,6 +1116,7 @@
|
|
|
883
1116
|
});
|
|
884
1117
|
if (data.user) {
|
|
885
1118
|
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
1119
|
+
persistSessionInBrowser(currentUser.sessionKey, currentUser.email, currentUser.isAdmin, currentUser);
|
|
886
1120
|
onAuthenticated();
|
|
887
1121
|
}
|
|
888
1122
|
}
|
|
@@ -891,6 +1125,71 @@
|
|
|
891
1125
|
}
|
|
892
1126
|
}
|
|
893
1127
|
|
|
1128
|
+
function updateServerAddonConfig(licenseId, patch = {}, { focusInput = false } = {}) {
|
|
1129
|
+
const prev = openServerAddonBuilders.get(licenseId) || { seats: 10, tier: 'business', years: 1 };
|
|
1130
|
+
const next = {
|
|
1131
|
+
seats: Math.max(1, Math.min(2000, Math.floor(Number(patch.seats ?? prev.seats) || 10))),
|
|
1132
|
+
tier: String(patch.tier ?? prev.tier ?? 'business'),
|
|
1133
|
+
years: Math.max(1, Math.min(5, Math.floor(Number(patch.years ?? prev.years) || 1))),
|
|
1134
|
+
};
|
|
1135
|
+
openServerAddonBuilders.set(licenseId, next);
|
|
1136
|
+
renderCabinetServers();
|
|
1137
|
+
if (focusInput) {
|
|
1138
|
+
const inp = $(`[data-addon-seats-input="${licenseId}"]`);
|
|
1139
|
+
if (inp) {
|
|
1140
|
+
inp.focus();
|
|
1141
|
+
try { inp.setSelectionRange(inp.value.length, inp.value.length); } catch { /* ignore */ }
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
async function submitServerAddonOrder(licenseId, payMode = 'yookassa') {
|
|
1147
|
+
const cfg = openServerAddonBuilders.get(licenseId) || { seats: 10, tier: 'business', years: 1 };
|
|
1148
|
+
const q = calcInlineAddonQuote(cfg);
|
|
1149
|
+
const isDirectInvoice = payMode === 'invoice' && Boolean(cleanCompanyName(currentUser?.companyName) && currentUser?.inn);
|
|
1150
|
+
const popupWin = isDirectInvoice ? window.open('about:blank', '_blank') : null;
|
|
1151
|
+
try {
|
|
1152
|
+
const data = await api('/api/cabinet/orders/draft', {
|
|
1153
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
1154
|
+
licenseId,
|
|
1155
|
+
includeBasePak: false,
|
|
1156
|
+
includeBaseLicenseOnly: false,
|
|
1157
|
+
tier: q.tier.id,
|
|
1158
|
+
addonSeats: q.seats,
|
|
1159
|
+
years: q.years,
|
|
1160
|
+
paymentMethod: payMode === 'invoice' ? 'invoice' : 'yookassa',
|
|
1161
|
+
});
|
|
1162
|
+
openServerAddonBuilders.delete(licenseId);
|
|
1163
|
+
if (data.user) {
|
|
1164
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
1165
|
+
persistSessionInBrowser(currentUser.sessionKey, currentUser.email, currentUser.isAdmin, currentUser);
|
|
1166
|
+
}
|
|
1167
|
+
const createdOrderId = data.order?.orderId;
|
|
1168
|
+
if (payMode === 'invoice' && createdOrderId) {
|
|
1169
|
+
openInvoiceOrderIds.add(createdOrderId);
|
|
1170
|
+
onAuthenticated();
|
|
1171
|
+
if (isDirectInvoice && (data.invoiceUrl || data.order?.invoiceUrl)) {
|
|
1172
|
+
const invUrl = data.invoiceUrl || data.order.invoiceUrl;
|
|
1173
|
+
if (popupWin) popupWin.location.href = invUrl;
|
|
1174
|
+
else window.open(invUrl, '_blank');
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
if (popupWin) popupWin.close();
|
|
1178
|
+
setTimeout(() => $(`#invCompany-${createdOrderId}`)?.focus(), 40);
|
|
1179
|
+
}
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
onAuthenticated();
|
|
1183
|
+
if (payMode === 'yookassa' && createdOrderId) {
|
|
1184
|
+
await payCabinetOrder(createdOrderId, 'yookassa');
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
catch (err) {
|
|
1188
|
+
if (popupWin) popupWin.close();
|
|
1189
|
+
alert(err.message);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
894
1193
|
async function payFranchiseInstallment(reservationId, { monthNumber = null, earlyPayoff = false, paymentMethod = 'yookassa' } = {}) {
|
|
895
1194
|
try {
|
|
896
1195
|
const data = await api('/api/cabinet/franchises/pay-installment', {
|
|
@@ -1046,16 +1345,38 @@
|
|
|
1046
1345
|
if (!orderId) return;
|
|
1047
1346
|
let companyName = '';
|
|
1048
1347
|
let inn = '';
|
|
1348
|
+
let kpp = '';
|
|
1349
|
+
let legalAddress = '';
|
|
1350
|
+
const errBox = $(`#invErr-${orderId}`);
|
|
1351
|
+
if (errBox) {
|
|
1352
|
+
errBox.style.display = 'none';
|
|
1353
|
+
errBox.textContent = '';
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1049
1356
|
if (paymentMethod === 'invoice') {
|
|
1050
|
-
|
|
1357
|
+
openInvoiceOrderIds.add(orderId);
|
|
1358
|
+
const invBox = $(`#invBox-${orderId}`);
|
|
1359
|
+
if (invBox) invBox.style.display = '';
|
|
1360
|
+
companyName = cleanCompanyName($(`#invCompany-${orderId}`)?.value) || cleanCompanyName(currentUser?.companyName);
|
|
1051
1361
|
inn = $(`#invInn-${orderId}`)?.value.trim() || currentUser?.inn || '';
|
|
1362
|
+
kpp = $(`#invKpp-${orderId}`)?.value.trim() || currentUser?.kpp || '';
|
|
1363
|
+
legalAddress = $(`#invAddr-${orderId}`)?.value.trim() || currentUser?.legalAddress || '';
|
|
1364
|
+
|
|
1052
1365
|
if (!companyName || !inn) {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1366
|
+
if (errBox) {
|
|
1367
|
+
errBox.style.display = '';
|
|
1368
|
+
errBox.textContent = !companyName
|
|
1369
|
+
? 'Укажите наименование организации или ИП (например, ООО «Маслоторг») для выставления счёта-договора.'
|
|
1370
|
+
: 'Укажите ИНН организации (10 или 12 цифр) для выставления счёта-договора.';
|
|
1371
|
+
}
|
|
1372
|
+
(!companyName ? $(`#invCompany-${orderId}`) : $(`#invInn-${orderId}`))?.focus();
|
|
1056
1373
|
return;
|
|
1057
1374
|
}
|
|
1058
1375
|
}
|
|
1376
|
+
|
|
1377
|
+
// Открываем вкладку синхронно в момент клика пользователя (до await), чтобы браузер не блокировал окно
|
|
1378
|
+
const popupWin = window.open('about:blank', '_blank');
|
|
1379
|
+
|
|
1059
1380
|
try {
|
|
1060
1381
|
const data = await api('/api/cabinet/orders/pay', {
|
|
1061
1382
|
sessionKey: currentUser?.sessionKey || '',
|
|
@@ -1063,16 +1384,32 @@
|
|
|
1063
1384
|
paymentMethod,
|
|
1064
1385
|
companyName,
|
|
1065
1386
|
inn,
|
|
1387
|
+
kpp,
|
|
1388
|
+
legalAddress,
|
|
1066
1389
|
});
|
|
1067
1390
|
if (data.user) {
|
|
1068
|
-
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
1391
|
+
currentUser = { ...data.user, sessionKey: data.user.sessionKey || currentUser.sessionKey };
|
|
1392
|
+
persistSessionInBrowser(currentUser.sessionKey, currentUser.email, currentUser.isAdmin, currentUser);
|
|
1069
1393
|
onAuthenticated();
|
|
1070
1394
|
}
|
|
1071
|
-
const url = data.invoiceUrl || data.confirmationUrl;
|
|
1072
|
-
if (url)
|
|
1395
|
+
const url = data.invoiceUrl || data.confirmationUrl || (paymentMethod === 'invoice' ? `/api/orders/${encodeURIComponent(orderId)}/invoice` : '');
|
|
1396
|
+
if (url) {
|
|
1397
|
+
if (popupWin) popupWin.location.href = url;
|
|
1398
|
+
else location.href = url;
|
|
1399
|
+
}
|
|
1400
|
+
else if (popupWin) {
|
|
1401
|
+
popupWin.close();
|
|
1402
|
+
}
|
|
1073
1403
|
}
|
|
1074
1404
|
catch (err) {
|
|
1075
|
-
|
|
1405
|
+
if (popupWin) popupWin.close();
|
|
1406
|
+
if (errBox) {
|
|
1407
|
+
errBox.style.display = '';
|
|
1408
|
+
errBox.textContent = err.message;
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
alert(err.message);
|
|
1412
|
+
}
|
|
1076
1413
|
}
|
|
1077
1414
|
}
|
|
1078
1415
|
|
|
@@ -1766,15 +2103,26 @@
|
|
|
1766
2103
|
/* ================= Привязка событий ================= */
|
|
1767
2104
|
function bind() {
|
|
1768
2105
|
document.addEventListener('click', (e) => {
|
|
1769
|
-
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');
|
|
2106
|
+
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-close-addon-builder],[data-addon-preset],[data-addon-tier],[data-addon-years],[data-submit-addon],[data-pay-installment],[data-cancel-franchise],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
|
|
1770
2107
|
if (!el) return;
|
|
1771
2108
|
if (el.matches('[data-cab-tab]')) { e.preventDefault(); switchCabinetTab(el.dataset.cabTab); return; }
|
|
1772
2109
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1773
2110
|
if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
|
|
1774
2111
|
if (el.matches('[data-invoice-toggle]')) {
|
|
1775
2112
|
e.stopPropagation();
|
|
1776
|
-
const
|
|
1777
|
-
|
|
2113
|
+
const oid = el.dataset.invoiceToggle;
|
|
2114
|
+
const box = $(`#invBox-${oid}`);
|
|
2115
|
+
if (box) {
|
|
2116
|
+
const nextVisible = box.style.display === 'none';
|
|
2117
|
+
box.style.display = nextVisible ? '' : 'none';
|
|
2118
|
+
if (nextVisible) {
|
|
2119
|
+
openInvoiceOrderIds.add(oid);
|
|
2120
|
+
setTimeout(() => $(`#invCompany-${oid}`)?.focus(), 30);
|
|
2121
|
+
}
|
|
2122
|
+
else {
|
|
2123
|
+
openInvoiceOrderIds.delete(oid);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1778
2126
|
return;
|
|
1779
2127
|
}
|
|
1780
2128
|
if (el.matches('[data-cancel-order]')) { e.stopPropagation(); cancelCabinetOrder(el.dataset.cancelOrder); return; }
|
|
@@ -1784,6 +2132,32 @@
|
|
|
1784
2132
|
if (el.matches('[data-delete-server]')) { e.stopPropagation(); deleteCabinetServer(el.dataset.deleteServer); return; }
|
|
1785
2133
|
if (el.matches('[data-download-lic]')) { e.stopPropagation(); downloadServerLicFile(el.dataset.downloadLic, el.dataset.fileIdx || 0); return; }
|
|
1786
2134
|
if (el.matches('[data-buy-for-server]')) { e.stopPropagation(); buyForSpecificServer(el.dataset.buyForServer, el.dataset.buyVariant || 'license_only'); return; }
|
|
2135
|
+
if (el.matches('[data-close-addon-builder]')) {
|
|
2136
|
+
e.stopPropagation();
|
|
2137
|
+
openServerAddonBuilders.delete(el.dataset.closeAddonBuilder);
|
|
2138
|
+
renderCabinetServers();
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
if (el.matches('[data-addon-preset]')) {
|
|
2142
|
+
e.stopPropagation();
|
|
2143
|
+
updateServerAddonConfig(el.dataset.addonPreset, { seats: Number(el.dataset.seats || 10) });
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (el.matches('[data-addon-tier]')) {
|
|
2147
|
+
e.stopPropagation();
|
|
2148
|
+
updateServerAddonConfig(el.dataset.addonTier, { tier: el.dataset.tier || 'business' });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (el.matches('[data-addon-years]')) {
|
|
2152
|
+
e.stopPropagation();
|
|
2153
|
+
updateServerAddonConfig(el.dataset.addonYears, { years: Number(el.dataset.years || 1) });
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (el.matches('[data-submit-addon]')) {
|
|
2157
|
+
e.stopPropagation();
|
|
2158
|
+
submitServerAddonOrder(el.dataset.submitAddon, el.dataset.payMode || 'yookassa');
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
1787
2161
|
if (el.matches('[data-pay-installment]')) {
|
|
1788
2162
|
e.stopPropagation();
|
|
1789
2163
|
payFranchiseInstallment(el.dataset.payInstallment, {
|
|
@@ -1841,6 +2215,7 @@
|
|
|
1841
2215
|
break;
|
|
1842
2216
|
case 'buy-client-config': buyClientConfig(); break;
|
|
1843
2217
|
case 'create-server': createCabinetServer(); break;
|
|
2218
|
+
case 'save-requisites': saveCabinetRequisites(); break;
|
|
1844
2219
|
case 'gen-hub-token': generateCabinetToken(); break;
|
|
1845
2220
|
case 'refresh-cabinet': refreshCabinetUser(); break;
|
|
1846
2221
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
@@ -1864,6 +2239,31 @@
|
|
|
1864
2239
|
else sendAuthCode();
|
|
1865
2240
|
}
|
|
1866
2241
|
});
|
|
2242
|
+
document.addEventListener('input', (e) => {
|
|
2243
|
+
const inp = e.target.closest?.('[data-addon-seats-input]');
|
|
2244
|
+
if (!inp) return;
|
|
2245
|
+
const lid = inp.dataset.addonSeatsInput;
|
|
2246
|
+
const prev = openServerAddonBuilders.get(lid) || { seats: 10, tier: 'business', years: 1 };
|
|
2247
|
+
const seats = Math.max(1, Math.min(2000, Math.floor(Number(inp.value) || 1)));
|
|
2248
|
+
prev.seats = seats;
|
|
2249
|
+
openServerAddonBuilders.set(lid, prev);
|
|
2250
|
+
const q = calcInlineAddonQuote(prev);
|
|
2251
|
+
const sumBox = $(`#addonQuoteSummary-${lid}`);
|
|
2252
|
+
if (sumBox) {
|
|
2253
|
+
sumBox.innerHTML = `<div class="small">
|
|
2254
|
+
Выбрано: <b>${q.seats} раб. мест</b> × <b>${q.years} г.</b> · Тариф <b>${esc(q.tier.name)}</b> · Цена за место: <b>${rub(q.perSeat)}</b>${q.disc > 0 ? ` <span class="tag ok" style="margin-left:4px">скидка −${q.disc}% (−${rub(q.saved)})</span>` : ''}
|
|
2255
|
+
</div>
|
|
2256
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
2257
|
+
<span style="font-size:15px;font-weight:800;margin-right:4px">Итого: ${rub(q.net)}</span>
|
|
2258
|
+
<button type="button" class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:12.5px" data-submit-addon="${esc(lid)}" data-pay-mode="yookassa">Оплатить ЮKassa</button>
|
|
2259
|
+
<button type="button" class="btn btn-ghost btn-sm" style="padding:6px 12px;font-size:12.5px" data-submit-addon="${esc(lid)}" data-pay-mode="invoice">Счёт юрлицу</button>
|
|
2260
|
+
</div>`;
|
|
2261
|
+
}
|
|
2262
|
+
$$(`[data-addon-preset="${lid}"]`).forEach(btn => {
|
|
2263
|
+
btn.classList.toggle('btn-primary', Number(btn.dataset.seats) === q.seats);
|
|
2264
|
+
btn.classList.toggle('btn-ghost', Number(btn.dataset.seats) !== q.seats);
|
|
2265
|
+
});
|
|
2266
|
+
});
|
|
1867
2267
|
$('#aiInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
|
|
1868
2268
|
$('#inpPakExtraSeats')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
|
|
1869
2269
|
$('#inpPakExtraSeatsNum')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
|
package/package.json
CHANGED
package/src/catalog.js
CHANGED
|
@@ -315,7 +315,7 @@ export function calculateOrderQuote({
|
|
|
315
315
|
const discSuffix = volumeDiscountPercent > 0 ? `, скидка −${volumeDiscountPercent}% за объём` : '';
|
|
316
316
|
items.push({
|
|
317
317
|
sku: `WORK-LIC-ADDON-${tierSpec.id.toUpperCase()}`,
|
|
318
|
-
title:
|
|
318
|
+
title: `Дополнительные лицензии ODANT-WORK: ${seats} раб. мест × ${periodYears} г. (уровень ${tierSpec.name}, ${effectiveUnitPriceRub.toLocaleString('ru-RU')} ₽/место${discSuffix})`,
|
|
319
319
|
quantity: seats,
|
|
320
320
|
unitPriceRub: effectiveUnitPriceRub,
|
|
321
321
|
totalRub: addonsAmountRub,
|
package/src/landing.js
CHANGED
|
@@ -343,6 +343,8 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
343
343
|
<aside class="sec-nav" aria-label="Разделы Личного кабинета">
|
|
344
344
|
<button type="button" class="sec-nav-item active" data-cab-tab="servers" id="navCabServers">Серверы и устройства (0)</button>
|
|
345
345
|
<button type="button" class="sec-nav-item" data-cab-tab="franchises" id="navCabFranchises">Региональные франшизы (0)</button>
|
|
346
|
+
<div class="sec-nav-sep"></div>
|
|
347
|
+
<button type="button" class="sec-nav-item" data-cab-tab="requisites" id="navCabRequisites">Реквизиты организации</button>
|
|
346
348
|
</aside>
|
|
347
349
|
|
|
348
350
|
<div class="sec-main">
|
|
@@ -368,6 +370,77 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
368
370
|
</div>
|
|
369
371
|
<div class="sec-box" id="cabinetFranchisesRegistry"></div>
|
|
370
372
|
</div>
|
|
373
|
+
|
|
374
|
+
<!-- 1.3 РЕКВИЗИТЫ ОРГАНИЗАЦИИ (ЮРЛИЦА / ИП) ДЛЯ СЧЕТОВ И ДОГОВОРОВ -->
|
|
375
|
+
<div id="cabSecRequisites" style="display:none">
|
|
376
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
377
|
+
<h2 class="sec-title">Реквизиты организации (Лицензиата)</h2>
|
|
378
|
+
<div class="sec-controls">
|
|
379
|
+
<button type="button" class="btn btn-primary btn-sm" data-action="save-requisites">Сохранить реквизиты</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="sec-box">
|
|
383
|
+
<div class="sec-item">
|
|
384
|
+
<div class="small muted" style="margin-bottom:14px">
|
|
385
|
+
Сохранённые реквизиты автоматически подставляются во все счета-договоры и закрывающие документы в Личном кабинете.
|
|
386
|
+
</div>
|
|
387
|
+
<div class="grid g2" style="gap:14px">
|
|
388
|
+
<div class="field" style="margin:0">
|
|
389
|
+
<label for="reqCompanyName">Наименование организации / ИП *</label>
|
|
390
|
+
<input type="text" id="reqCompanyName" placeholder="ООО «Маслоторг»">
|
|
391
|
+
</div>
|
|
392
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
393
|
+
<div class="field" style="margin:0">
|
|
394
|
+
<label for="reqInn">ИНН *</label>
|
|
395
|
+
<input type="text" id="reqInn" placeholder="10 или 12 цифр" maxlength="12">
|
|
396
|
+
</div>
|
|
397
|
+
<div class="field" style="margin:0">
|
|
398
|
+
<label for="reqKpp">КПП (для ООО)</label>
|
|
399
|
+
<input type="text" id="reqKpp" placeholder="9 цифр" maxlength="9">
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="field" style="margin:0;grid-column:1 / -1">
|
|
403
|
+
<label for="reqLegalAddress">Юридический адрес</label>
|
|
404
|
+
<input type="text" id="reqLegalAddress" placeholder="Индекс, город, улица, дом, офис">
|
|
405
|
+
</div>
|
|
406
|
+
<div class="field" style="margin:0">
|
|
407
|
+
<label for="reqSignerPosition">Должность руководителя (подписанта)</label>
|
|
408
|
+
<input type="text" id="reqSignerPosition" placeholder="Генеральный директор">
|
|
409
|
+
</div>
|
|
410
|
+
<div class="field" style="margin:0">
|
|
411
|
+
<label for="reqSignerName">ФИО руководителя (подписанта)</label>
|
|
412
|
+
<input type="text" id="reqSignerName" placeholder="Иванов Иван Иванович">
|
|
413
|
+
</div>
|
|
414
|
+
<div class="field" style="margin:0">
|
|
415
|
+
<label for="reqBankName">Наименование банка</label>
|
|
416
|
+
<input type="text" id="reqBankName" placeholder="ПАО Сбербанк">
|
|
417
|
+
</div>
|
|
418
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
419
|
+
<div class="field" style="margin:0">
|
|
420
|
+
<label for="reqBik">БИК</label>
|
|
421
|
+
<input type="text" id="reqBik" placeholder="9 цифр" maxlength="9">
|
|
422
|
+
</div>
|
|
423
|
+
<div class="field" style="margin:0">
|
|
424
|
+
<label for="reqPhone">Контактный телефон</label>
|
|
425
|
+
<input type="text" id="reqPhone" placeholder="+7 (___) ___-__-__">
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
<div class="field" style="margin:0">
|
|
429
|
+
<label for="reqBankAccount">Расчётный счёт (р/с)</label>
|
|
430
|
+
<input type="text" id="reqBankAccount" placeholder="40702810..." maxlength="20">
|
|
431
|
+
</div>
|
|
432
|
+
<div class="field" style="margin:0">
|
|
433
|
+
<label for="reqCorrAccount">Корреспондентский счёт (к/с)</label>
|
|
434
|
+
<input type="text" id="reqCorrAccount" placeholder="30101810..." maxlength="20">
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:16px;gap:12px;flex-wrap:wrap">
|
|
438
|
+
<div class="small" id="reqSaveStatus" style="color:var(--ok);font-weight:600"></div>
|
|
439
|
+
<button type="button" class="btn btn-primary" data-action="save-requisites">Сохранить реквизиты в Личном кабинете</button>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
371
444
|
</div>
|
|
372
445
|
</div>
|
|
373
446
|
</section>
|
package/src/server.js
CHANGED
|
@@ -539,6 +539,14 @@ export function createLicenseServerApp(options = {}) {
|
|
|
539
539
|
return sendJson(res, 200, result);
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/profile') {
|
|
543
|
+
const body = await readJsonBody(req);
|
|
544
|
+
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
545
|
+
const user = store.updateUserProfile(key, body);
|
|
546
|
+
res.setHeader('Set-Cookie', `odant_work_session=${encodeURIComponent(user.sessionKey)}; Path=/; Max-Age=31536000; SameSite=Lax`);
|
|
547
|
+
return sendJson(res, 200, { ok: true, user });
|
|
548
|
+
}
|
|
549
|
+
|
|
542
550
|
if (req.method === 'POST' && pathname === '/api/auth/accept-oferta') {
|
|
543
551
|
const body = await readJsonBody(req);
|
|
544
552
|
const key = extractBearerToken(req, body, url) || body.email || '';
|
|
@@ -641,8 +649,16 @@ export function createLicenseServerApp(options = {}) {
|
|
|
641
649
|
createNewServer: Boolean(body.createNewServer || body.licenseId === 'NEW'),
|
|
642
650
|
ownerAccountId: user?.userId || '',
|
|
643
651
|
customer: {
|
|
644
|
-
name: body.customer?.name || user?.companyName ||
|
|
652
|
+
name: body.customer?.name || user?.companyName || '',
|
|
645
653
|
inn: body.customer?.inn || user?.inn || '',
|
|
654
|
+
kpp: body.customer?.kpp || user?.kpp || '',
|
|
655
|
+
legalAddress: body.customer?.legalAddress || user?.legalAddress || '',
|
|
656
|
+
signerName: body.customer?.signerName || user?.signerName || '',
|
|
657
|
+
signerPosition: body.customer?.signerPosition || user?.signerPosition || '',
|
|
658
|
+
bankName: body.customer?.bankName || user?.bankName || '',
|
|
659
|
+
bik: body.customer?.bik || user?.bik || '',
|
|
660
|
+
bankAccount: body.customer?.bankAccount || user?.bankAccount || '',
|
|
661
|
+
corrAccount: body.customer?.corrAccount || user?.corrAccount || '',
|
|
646
662
|
email: body.customer?.email || user?.email || '',
|
|
647
663
|
phone: body.customer?.phone || user?.phone || '',
|
|
648
664
|
},
|
|
@@ -652,6 +668,10 @@ export function createLicenseServerApp(options = {}) {
|
|
|
652
668
|
return sendJson(res, 200, {
|
|
653
669
|
ok: true,
|
|
654
670
|
...(payment || {}),
|
|
671
|
+
...(order?.paymentMethod === 'invoice' ? {
|
|
672
|
+
invoiceUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
673
|
+
confirmationUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
674
|
+
} : {}),
|
|
655
675
|
order,
|
|
656
676
|
reservation,
|
|
657
677
|
user: updatedUser,
|
|
@@ -715,8 +735,20 @@ export function createLicenseServerApp(options = {}) {
|
|
|
715
735
|
paymentMethod: body.paymentMethod || 'yookassa',
|
|
716
736
|
companyName: body.companyName || user?.companyName || '',
|
|
717
737
|
inn: body.inn || user?.inn || '',
|
|
738
|
+
kpp: body.kpp ?? user?.kpp ?? '',
|
|
739
|
+
legalAddress: body.legalAddress ?? user?.legalAddress ?? '',
|
|
740
|
+
signerName: body.signerName ?? user?.signerName ?? '',
|
|
741
|
+
signerPosition: body.signerPosition ?? user?.signerPosition ?? '',
|
|
742
|
+
bankName: body.bankName ?? user?.bankName ?? '',
|
|
743
|
+
bik: body.bik ?? user?.bik ?? '',
|
|
744
|
+
bankAccount: body.bankAccount ?? user?.bankAccount ?? '',
|
|
745
|
+
corrAccount: body.corrAccount ?? user?.corrAccount ?? '',
|
|
746
|
+
phone: body.phone ?? user?.phone ?? '',
|
|
718
747
|
});
|
|
719
748
|
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
749
|
+
if (updatedUser?.sessionKey) {
|
|
750
|
+
res.setHeader('Set-Cookie', `odant_work_session=${encodeURIComponent(updatedUser.sessionKey)}; Path=/; Max-Age=31536000; SameSite=Lax`);
|
|
751
|
+
}
|
|
720
752
|
if (order.paymentMethod === 'invoice') {
|
|
721
753
|
return sendJson(res, 200, {
|
|
722
754
|
ok: true,
|
package/src/store.js
CHANGED
|
@@ -789,10 +789,16 @@ export class LicenseStore {
|
|
|
789
789
|
contractNumber: method === 'invoice' ? `ДОГ-${dateTag}-${shortRand}` : '',
|
|
790
790
|
invoiceUrl: method === 'invoice' ? `/api/orders/${orderId}/invoice` : '',
|
|
791
791
|
customer: {
|
|
792
|
-
name: customer.name
|
|
792
|
+
name: (customer.name && !String(customer.name).includes('@')) ? customer.name : ((targetLic.customer?.name && !String(targetLic.customer.name).includes('@')) ? targetLic.customer.name : ''),
|
|
793
793
|
inn: customer.inn || targetLic.customer?.inn || '',
|
|
794
|
-
kpp: customer.kpp || '',
|
|
795
|
-
legalAddress: customer.legalAddress || '',
|
|
794
|
+
kpp: customer.kpp || targetLic.customer?.kpp || '',
|
|
795
|
+
legalAddress: customer.legalAddress || targetLic.customer?.legalAddress || '',
|
|
796
|
+
signerName: customer.signerName || targetLic.customer?.signerName || '',
|
|
797
|
+
signerPosition: customer.signerPosition || targetLic.customer?.signerPosition || '',
|
|
798
|
+
bankName: customer.bankName || targetLic.customer?.bankName || '',
|
|
799
|
+
bik: customer.bik || targetLic.customer?.bik || '',
|
|
800
|
+
bankAccount: customer.bankAccount || targetLic.customer?.bankAccount || '',
|
|
801
|
+
corrAccount: customer.corrAccount || targetLic.customer?.corrAccount || '',
|
|
796
802
|
email: customer.email || targetLic.customer?.email || 'billing@odant.org',
|
|
797
803
|
phone: customer.phone || targetLic.customer?.phone || '',
|
|
798
804
|
},
|
|
@@ -977,11 +983,13 @@ export class LicenseStore {
|
|
|
977
983
|
if (!ord)
|
|
978
984
|
return null;
|
|
979
985
|
const dateStr = new Date(ord.createdAt || Date.now()).toLocaleDateString('ru-RU');
|
|
986
|
+
const c = ord.customer || {};
|
|
987
|
+
const custName = (c.name && !String(c.name).includes('@')) ? c.name : 'Организация (не указана)';
|
|
980
988
|
const rowsHtml = (ord.items || []).map((it, idx) => `
|
|
981
989
|
<tr>
|
|
982
990
|
<td style="text-align:center;">${idx + 1}</td>
|
|
983
991
|
<td>${it.title}</td>
|
|
984
|
-
<td style="text-align:center;">${it.quantity}</td>
|
|
992
|
+
<td style="text-align:center;font-weight:700;">${it.quantity}</td>
|
|
985
993
|
<td style="text-align:right;">${Number(it.unitPriceRub || 0).toLocaleString('ru-RU')} ₽</td>
|
|
986
994
|
<td style="text-align:right;font-weight:700;">${Number(it.totalRub || 0).toLocaleString('ru-RU')} ₽</td>
|
|
987
995
|
</tr>
|
|
@@ -993,50 +1001,81 @@ export class LicenseStore {
|
|
|
993
1001
|
<meta charset="utf-8">
|
|
994
1002
|
<title>Счёт-договор № ${ord.invoiceNumber || ord.orderId} от ${dateStr}</title>
|
|
995
1003
|
<style>
|
|
996
|
-
body { font-family: Arial, sans-serif; color: #111827; max-width:
|
|
997
|
-
h1 { font-size: 20px; margin
|
|
1004
|
+
body { font-family: Arial, sans-serif; color: #111827; max-width: 880px; margin: 20px auto; padding: 20px; line-height: 1.45; }
|
|
1005
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
998
1006
|
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13px; }
|
|
999
1007
|
th, td { border: 1px solid #cbd5e1; padding: 8px 10px; }
|
|
1000
1008
|
th { background: #f1f5f9; text-align: left; }
|
|
1001
|
-
.meta { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding:
|
|
1002
|
-
.status { display: inline-block; padding:
|
|
1009
|
+
.meta { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 14px 16px; font-size: 13px; }
|
|
1010
|
+
.status { display: inline-block; padding: 5px 12px; border-radius: 999px; font-size: 12px; font-weight: 700; background: ${ord.status === 'paid' ? '#dcfce7' : '#fef3c7'}; color: ${ord.status === 'paid' ? '#166534' : '#92400e'}; }
|
|
1011
|
+
.toolbar { display: flex; justify-content: space-between; align-items: center; background: #0f172a; color: #f8fafc; padding: 10px 16px; border-radius: 10px; margin-bottom: 18px; }
|
|
1012
|
+
.toolbar button { background: #2563eb; color: #fff; border: none; border-radius: 6px; padding: 8px 14px; font-size: 13px; font-weight: 700; cursor: pointer; }
|
|
1013
|
+
.toolbar button.sec { background: #334155; margin-left: 8px; }
|
|
1014
|
+
.sig-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 28px; font-size: 13px; }
|
|
1015
|
+
.sig-box { border-top: 1px solid #cbd5e1; padding-top: 10px; }
|
|
1016
|
+
@media print { .no-print { display: none !important; } body { margin: 0; padding: 10px; } }
|
|
1003
1017
|
</style>
|
|
1004
1018
|
</head>
|
|
1005
1019
|
<body>
|
|
1006
|
-
<div
|
|
1020
|
+
<div class="toolbar no-print">
|
|
1021
|
+
<div style="font-size:13.5px;font-weight:700;">Счёт-договор № ${ord.invoiceNumber || ord.orderId} · ODANT-WORK</div>
|
|
1022
|
+
<div>
|
|
1023
|
+
<button type="button" onclick="window.print()">Печать / Сохранить в PDF</button>
|
|
1024
|
+
<button type="button" class="sec" onclick="window.close()">Закрыть вкладку</button>
|
|
1025
|
+
</div>
|
|
1026
|
+
</div>
|
|
1027
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;">
|
|
1007
1028
|
<div>
|
|
1008
1029
|
<h1>Счёт-договор № ${ord.invoiceNumber || ord.orderId} от ${dateStr}</h1>
|
|
1009
|
-
<div>Договор № ${ord.contractNumber || ord.orderId} · Заказ № ${ord.orderId}</div>
|
|
1030
|
+
<div style="font-size:13px;color:#475569;">Договор № ${ord.contractNumber || ord.orderId} · Заказ № ${ord.orderId} · Сервер: ${ord.licenseId || '—'}</div>
|
|
1010
1031
|
</div>
|
|
1011
1032
|
<span class="status">${ord.status === 'paid' ? 'ОПЛАЧЕН' : 'ОЖИДАЕТ ОПЛАТЫ ПО СЧЁТУ'}</span>
|
|
1012
1033
|
</div>
|
|
1013
1034
|
<div class="meta" style="margin-top:14px;">
|
|
1014
|
-
<div><strong>Поставщик (
|
|
1015
|
-
<div style="margin-top:
|
|
1035
|
+
<div><strong>Поставщик (Лицензиар):</strong> ООО «${VENDOR_CONTACTS.company}», адрес: ${VENDOR_CONTACTS.address}, тел.: ${VENDOR_CONTACTS.phones.join(', ')}</div>
|
|
1036
|
+
<div style="margin-top:8px;">
|
|
1037
|
+
<strong>Покупатель (Лицензиат):</strong> ${custName}, ИНН: ${c.inn || '—'}${c.kpp ? ', КПП: ' + c.kpp : ''}
|
|
1038
|
+
${c.legalAddress ? `<br><strong>Юридический адрес:</strong> ${c.legalAddress}` : ''}
|
|
1039
|
+
${c.bankName || c.bankAccount ? `<br><strong>Банковские реквизиты:</strong> р/с ${c.bankAccount || '—'} в ${c.bankName || '—'}${c.bik ? ', БИК ' + c.bik : ''}${c.corrAccount ? ', к/с ' + c.corrAccount : ''}` : ''}
|
|
1040
|
+
${c.signerName ? `<br><strong>Уполномоченное лицо:</strong> ${c.signerPosition ? c.signerPosition + ' ' : ''}${c.signerName}` : ''}
|
|
1041
|
+
<br><strong>Контакты:</strong> Email: ${c.email || '—'}${c.phone ? ', тел.: ' + c.phone : ''}
|
|
1042
|
+
</div>
|
|
1016
1043
|
</div>
|
|
1017
1044
|
<table>
|
|
1018
1045
|
<thead>
|
|
1019
1046
|
<tr>
|
|
1020
1047
|
<th style="width:40px;">№</th>
|
|
1021
|
-
<th>Наименование
|
|
1022
|
-
<th style="width:
|
|
1023
|
-
<th style="width:130px;"
|
|
1024
|
-
<th style="width:140px;">Сумма</th>
|
|
1048
|
+
<th>Наименование лицензии / программно-аппаратного комплекса / услуги</th>
|
|
1049
|
+
<th style="width:85px;text-align:center;">Кол-во мест / шт.</th>
|
|
1050
|
+
<th style="width:130px;text-align:right;">Цена за ед.</th>
|
|
1051
|
+
<th style="width:140px;text-align:right;">Сумма</th>
|
|
1025
1052
|
</tr>
|
|
1026
1053
|
</thead>
|
|
1027
1054
|
<tbody>${rowsHtml}</tbody>
|
|
1028
1055
|
<tfoot>
|
|
1029
1056
|
<tr>
|
|
1030
|
-
<td colspan="4" style="text-align:right;font-weight:700;">ИТОГО К ОПЛАТЕ (
|
|
1057
|
+
<td colspan="4" style="text-align:right;font-weight:700;">ИТОГО К ОПЛАТЕ (НДС не облагается на основании пп. 26 п. 2 ст. 149 НК РФ / УСН):</td>
|
|
1031
1058
|
<td style="text-align:right;font-weight:800;font-size:15px;">${Number(ord.amountRub || 0).toLocaleString('ru-RU')} ₽</td>
|
|
1032
1059
|
</tr>
|
|
1033
1060
|
</tfoot>
|
|
1034
1061
|
</table>
|
|
1035
1062
|
<p style="font-size:12.5px;color:#475569;">
|
|
1036
|
-
Оплата настоящего счёта-договора означает
|
|
1037
|
-
|
|
1038
|
-
|
|
1063
|
+
1. Оплата настоящего счёта-договора означает акцепт Лицензиатом условий Публичного договора-оферты ODANT-WORK (${PUBLIC_OFERTA.version}).<br>
|
|
1064
|
+
2. В назначении платежа обязательно укажите: «Оплата по счёту-договору № ${ord.invoiceNumber || ord.orderId} от ${dateStr} за лицензии ODANT-WORK (заказ ${ord.orderId}). Без НДС».<br>
|
|
1065
|
+
3. После зачисления средств файлы лицензий (<code>.lic.json</code>) автоматически активируются в Личном кабинете (${c.email || ''}) для сервера <code>${ord.licenseId || ''}</code>.
|
|
1039
1066
|
</p>
|
|
1067
|
+
<div class="sig-grid">
|
|
1068
|
+
<div class="sig-box">
|
|
1069
|
+
<strong>От Поставщика (Лицензиара):</strong><br>
|
|
1070
|
+
ООО «${VENDOR_CONTACTS.company}»<br><br>
|
|
1071
|
+
______________________ / Руководитель /
|
|
1072
|
+
</div>
|
|
1073
|
+
<div class="sig-box">
|
|
1074
|
+
<strong>От Покупателя (Лицензиата):</strong><br>
|
|
1075
|
+
${custName}<br><br>
|
|
1076
|
+
______________________ / ${c.signerName || 'Руководитель'} /
|
|
1077
|
+
</div>
|
|
1078
|
+
</div>
|
|
1040
1079
|
</body>
|
|
1041
1080
|
</html>`;
|
|
1042
1081
|
}
|
|
@@ -1295,8 +1334,10 @@ export class LicenseStore {
|
|
|
1295
1334
|
ownerEmail: user.email,
|
|
1296
1335
|
hubUrl: hubUrl || user.hubUrl || 'https://work.odant.org',
|
|
1297
1336
|
customer: {
|
|
1298
|
-
name: user.companyName
|
|
1337
|
+
name: (user.companyName && !String(user.companyName).includes('@')) ? user.companyName : '',
|
|
1299
1338
|
inn: user.inn || '',
|
|
1339
|
+
kpp: user.kpp || '',
|
|
1340
|
+
legalAddress: user.legalAddress || '',
|
|
1300
1341
|
email: user.email,
|
|
1301
1342
|
phone: user.phone || '',
|
|
1302
1343
|
},
|
|
@@ -1452,6 +1493,7 @@ export class LicenseStore {
|
|
|
1452
1493
|
.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
1453
1494
|
const userFranchises = this.listUserFranchises(user, now);
|
|
1454
1495
|
const userServers = this.listUserServers(user, now);
|
|
1496
|
+
const cleanCompany = (user.companyName && !String(user.companyName).includes('@')) ? user.companyName : '';
|
|
1455
1497
|
return {
|
|
1456
1498
|
userId: user.userId,
|
|
1457
1499
|
email: user.email,
|
|
@@ -1460,8 +1502,16 @@ export class LicenseStore {
|
|
|
1460
1502
|
authProvider: user.authProvider || 'email',
|
|
1461
1503
|
name: user.name,
|
|
1462
1504
|
position: user.position || '',
|
|
1463
|
-
companyName:
|
|
1464
|
-
inn: user.inn,
|
|
1505
|
+
companyName: cleanCompany,
|
|
1506
|
+
inn: user.inn || '',
|
|
1507
|
+
kpp: user.kpp || '',
|
|
1508
|
+
legalAddress: user.legalAddress || '',
|
|
1509
|
+
signerName: user.signerName || '',
|
|
1510
|
+
signerPosition: user.signerPosition || user.position || '',
|
|
1511
|
+
bankName: user.bankName || '',
|
|
1512
|
+
bik: user.bik || '',
|
|
1513
|
+
bankAccount: user.bankAccount || '',
|
|
1514
|
+
corrAccount: user.corrAccount || '',
|
|
1465
1515
|
phone: user.phone || '',
|
|
1466
1516
|
licenseId: user.licenseId,
|
|
1467
1517
|
sessionKey: user.sessionKey,
|
|
@@ -1483,6 +1533,89 @@ export class LicenseStore {
|
|
|
1483
1533
|
};
|
|
1484
1534
|
}
|
|
1485
1535
|
|
|
1536
|
+
/**
|
|
1537
|
+
* Обновить и сохранить реквизиты организации (юридического лица / ИП) в профиле Личного кабинета.
|
|
1538
|
+
*/
|
|
1539
|
+
updateUserProfile(sessionKeyOrEmail = '', {
|
|
1540
|
+
companyName = undefined,
|
|
1541
|
+
inn = undefined,
|
|
1542
|
+
kpp = undefined,
|
|
1543
|
+
legalAddress = undefined,
|
|
1544
|
+
signerName = undefined,
|
|
1545
|
+
signerPosition = undefined,
|
|
1546
|
+
bankName = undefined,
|
|
1547
|
+
bik = undefined,
|
|
1548
|
+
bankAccount = undefined,
|
|
1549
|
+
corrAccount = undefined,
|
|
1550
|
+
phone = undefined,
|
|
1551
|
+
} = {}) {
|
|
1552
|
+
const user = this._findUser(sessionKeyOrEmail);
|
|
1553
|
+
if (!user)
|
|
1554
|
+
throw new Error('Пользователь не найден. Пожалуйста, войдите в Личный кабинет.');
|
|
1555
|
+
if (companyName !== undefined) {
|
|
1556
|
+
const c = String(companyName || '').trim();
|
|
1557
|
+
user.companyName = c.includes('@') ? '' : c;
|
|
1558
|
+
}
|
|
1559
|
+
if (inn !== undefined) user.inn = String(inn || '').trim();
|
|
1560
|
+
if (kpp !== undefined) user.kpp = String(kpp || '').trim();
|
|
1561
|
+
if (legalAddress !== undefined) user.legalAddress = String(legalAddress || '').trim();
|
|
1562
|
+
if (signerName !== undefined) user.signerName = String(signerName || '').trim();
|
|
1563
|
+
if (signerPosition !== undefined) {
|
|
1564
|
+
user.signerPosition = String(signerPosition || '').trim();
|
|
1565
|
+
user.position = user.signerPosition;
|
|
1566
|
+
}
|
|
1567
|
+
if (bankName !== undefined) user.bankName = String(bankName || '').trim();
|
|
1568
|
+
if (bik !== undefined) user.bik = String(bik || '').trim();
|
|
1569
|
+
if (bankAccount !== undefined) user.bankAccount = String(bankAccount || '').trim();
|
|
1570
|
+
if (corrAccount !== undefined) user.corrAccount = String(corrAccount || '').trim();
|
|
1571
|
+
if (phone !== undefined) user.phone = String(phone || '').trim();
|
|
1572
|
+
|
|
1573
|
+
// Синхронизируем реквизиты со всеми серверами и неоплаченными заказами этого клиента
|
|
1574
|
+
for (const lic of Object.values(this.state.licenses || {})) {
|
|
1575
|
+
if (!lic) continue;
|
|
1576
|
+
if (lic.ownerAccountId === user.userId || String(lic.ownerEmail || '').toLowerCase() === user.email) {
|
|
1577
|
+
lic.customer = lic.customer || {};
|
|
1578
|
+
if (user.companyName) lic.customer.name = user.companyName;
|
|
1579
|
+
if (user.inn) lic.customer.inn = user.inn;
|
|
1580
|
+
if (user.kpp !== undefined) lic.customer.kpp = user.kpp;
|
|
1581
|
+
if (user.legalAddress !== undefined) lic.customer.legalAddress = user.legalAddress;
|
|
1582
|
+
if (user.phone !== undefined) lic.customer.phone = user.phone;
|
|
1583
|
+
this.state.licenses[lic.licenseId] = signCertificate(lic, this.authorityKeys);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
for (const ord of Object.values(this.state.orders || {})) {
|
|
1587
|
+
if (!ord || ord.status === 'paid') continue;
|
|
1588
|
+
if (ord.ownerAccountId === user.userId || String(ord.customer?.email || '').toLowerCase() === user.email) {
|
|
1589
|
+
ord.customer = ord.customer || {};
|
|
1590
|
+
if (user.companyName) ord.customer.name = user.companyName;
|
|
1591
|
+
if (user.inn) ord.customer.inn = user.inn;
|
|
1592
|
+
if (user.kpp !== undefined) ord.customer.kpp = user.kpp;
|
|
1593
|
+
if (user.legalAddress !== undefined) ord.customer.legalAddress = user.legalAddress;
|
|
1594
|
+
if (user.signerName !== undefined) ord.customer.signerName = user.signerName;
|
|
1595
|
+
if (user.signerPosition !== undefined) ord.customer.signerPosition = user.signerPosition;
|
|
1596
|
+
if (user.bankName !== undefined) ord.customer.bankName = user.bankName;
|
|
1597
|
+
if (user.bik !== undefined) ord.customer.bik = user.bik;
|
|
1598
|
+
if (user.bankAccount !== undefined) ord.customer.bankAccount = user.bankAccount;
|
|
1599
|
+
if (user.corrAccount !== undefined) ord.customer.corrAccount = user.corrAccount;
|
|
1600
|
+
if (user.phone !== undefined) ord.customer.phone = user.phone;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Обновляем подписанный токен сессии с новыми реквизитами
|
|
1605
|
+
user.sessionKey = createSessionToken({
|
|
1606
|
+
userId: user.userId,
|
|
1607
|
+
email: user.email,
|
|
1608
|
+
name: user.name,
|
|
1609
|
+
companyName: user.companyName,
|
|
1610
|
+
inn: user.inn,
|
|
1611
|
+
isAdmin: Boolean(user.isAdmin),
|
|
1612
|
+
issuedAt: Date.now(),
|
|
1613
|
+
}, this.authorityKeys);
|
|
1614
|
+
|
|
1615
|
+
this._save();
|
|
1616
|
+
return this._formatUserResponse(user);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1486
1619
|
/**
|
|
1487
1620
|
* Зарегистрировать или авторизовать пользователя личного кабинета ODANT-WORK
|
|
1488
1621
|
* (по email, через OAuth или через мессенджеры Telegram / MAX / VK).
|
|
@@ -1515,6 +1648,7 @@ export class LicenseStore {
|
|
|
1515
1648
|
const userId = user?.userId || ('USR-' + crypto.createHash('sha256').update(cleanEmail).digest('hex').slice(0, 10).toUpperCase());
|
|
1516
1649
|
|
|
1517
1650
|
if (!user) {
|
|
1651
|
+
const safeCompany = (companyName && !String(companyName).includes('@')) ? String(companyName).trim() : '';
|
|
1518
1652
|
let lic = licenseId ? this.findLicense(licenseId) : null;
|
|
1519
1653
|
if (!lic) {
|
|
1520
1654
|
const summary = this.issueOrUpdateLicense({
|
|
@@ -1522,7 +1656,7 @@ export class LicenseStore {
|
|
|
1522
1656
|
ownerEmail: cleanEmail,
|
|
1523
1657
|
baseActivated: false,
|
|
1524
1658
|
customer: {
|
|
1525
|
-
name:
|
|
1659
|
+
name: safeCompany,
|
|
1526
1660
|
inn: inn || '',
|
|
1527
1661
|
email: cleanEmail,
|
|
1528
1662
|
phone: phone || '',
|
|
@@ -1541,10 +1675,18 @@ export class LicenseStore {
|
|
|
1541
1675
|
userId,
|
|
1542
1676
|
email: cleanEmail,
|
|
1543
1677
|
authProvider: cleanProvider,
|
|
1544
|
-
name: String(name ||
|
|
1678
|
+
name: String(name || safeCompany || cleanEmail.split('@')[0]).trim(),
|
|
1545
1679
|
position: String(position || '').trim(),
|
|
1546
|
-
companyName:
|
|
1680
|
+
companyName: safeCompany,
|
|
1547
1681
|
inn: String(inn || '').trim(),
|
|
1682
|
+
kpp: '',
|
|
1683
|
+
legalAddress: '',
|
|
1684
|
+
signerName: '',
|
|
1685
|
+
signerPosition: String(position || '').trim(),
|
|
1686
|
+
bankName: '',
|
|
1687
|
+
bik: '',
|
|
1688
|
+
bankAccount: '',
|
|
1689
|
+
corrAccount: '',
|
|
1548
1690
|
phone: String(phone || '').trim(),
|
|
1549
1691
|
licenseId: lic.licenseId,
|
|
1550
1692
|
sessionKey: 'wks_' + crypto.randomBytes(16).toString('hex'),
|
|
@@ -2285,7 +2427,7 @@ export class LicenseStore {
|
|
|
2285
2427
|
}
|
|
2286
2428
|
|
|
2287
2429
|
/**
|
|
2288
|
-
* Обновить способ оплаты и реквизиты юрлица для существующего заказа из Личного
|
|
2430
|
+
* Обновить способ оплаты и реквизиты юрлица для существующего заказа из Личного кабинета (и запомнить реквизиты в ЛК).
|
|
2289
2431
|
*/
|
|
2290
2432
|
prepareOrderPayment(orderId, {
|
|
2291
2433
|
paymentMethod = 'yookassa',
|
|
@@ -2293,16 +2435,31 @@ export class LicenseStore {
|
|
|
2293
2435
|
inn = '',
|
|
2294
2436
|
kpp = '',
|
|
2295
2437
|
legalAddress = '',
|
|
2438
|
+
signerName = '',
|
|
2439
|
+
signerPosition = '',
|
|
2440
|
+
bankName = '',
|
|
2441
|
+
bik = '',
|
|
2442
|
+
bankAccount = '',
|
|
2443
|
+
corrAccount = '',
|
|
2444
|
+
phone = '',
|
|
2296
2445
|
} = {}) {
|
|
2297
2446
|
const order = this.state.orders[orderId];
|
|
2298
2447
|
if (!order)
|
|
2299
2448
|
throw new Error(`Заказ ${orderId} не найден`);
|
|
2300
2449
|
order.paymentMethod = paymentMethod === 'invoice' ? 'invoice' : 'yookassa';
|
|
2301
2450
|
order.customer = order.customer || {};
|
|
2302
|
-
|
|
2451
|
+
const cleanComp = (companyName && !String(companyName).includes('@')) ? String(companyName).trim() : '';
|
|
2452
|
+
if (cleanComp) order.customer.name = cleanComp;
|
|
2303
2453
|
if (inn) order.customer.inn = String(inn).trim();
|
|
2304
|
-
if (kpp) order.customer.kpp = String(kpp).trim();
|
|
2305
|
-
if (legalAddress) order.customer.legalAddress = String(legalAddress).trim();
|
|
2454
|
+
if (kpp !== undefined && kpp !== '') order.customer.kpp = String(kpp).trim();
|
|
2455
|
+
if (legalAddress !== undefined && legalAddress !== '') order.customer.legalAddress = String(legalAddress).trim();
|
|
2456
|
+
if (signerName !== undefined && signerName !== '') order.customer.signerName = String(signerName).trim();
|
|
2457
|
+
if (signerPosition !== undefined && signerPosition !== '') order.customer.signerPosition = String(signerPosition).trim();
|
|
2458
|
+
if (bankName !== undefined && bankName !== '') order.customer.bankName = String(bankName).trim();
|
|
2459
|
+
if (bik !== undefined && bik !== '') order.customer.bik = String(bik).trim();
|
|
2460
|
+
if (bankAccount !== undefined && bankAccount !== '') order.customer.bankAccount = String(bankAccount).trim();
|
|
2461
|
+
if (corrAccount !== undefined && corrAccount !== '') order.customer.corrAccount = String(corrAccount).trim();
|
|
2462
|
+
if (phone !== undefined && phone !== '') order.customer.phone = String(phone).trim();
|
|
2306
2463
|
if (!order.invoiceNumber) {
|
|
2307
2464
|
order.invoiceNumber = 'СЧ-' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-' + orderId.slice(-4);
|
|
2308
2465
|
}
|
|
@@ -2313,14 +2470,26 @@ export class LicenseStore {
|
|
|
2313
2470
|
order.invoiceUrl = `/api/orders/${encodeURIComponent(orderId)}/invoice`;
|
|
2314
2471
|
}
|
|
2315
2472
|
if (order.reservationId && this.state.reservations[order.reservationId]) {
|
|
2316
|
-
if (
|
|
2473
|
+
if (cleanComp) this.state.reservations[order.reservationId].companyName = cleanComp;
|
|
2317
2474
|
if (inn) this.state.reservations[order.reservationId].inn = String(inn).trim();
|
|
2318
2475
|
}
|
|
2319
2476
|
if (order.ownerAccountId) {
|
|
2320
2477
|
const u = Object.values(this.state.users).find(usr => usr.userId === order.ownerAccountId);
|
|
2321
2478
|
if (u) {
|
|
2322
|
-
if (
|
|
2479
|
+
if (cleanComp) u.companyName = cleanComp;
|
|
2323
2480
|
if (inn) u.inn = String(inn).trim();
|
|
2481
|
+
if (kpp) u.kpp = String(kpp).trim();
|
|
2482
|
+
if (legalAddress) u.legalAddress = String(legalAddress).trim();
|
|
2483
|
+
if (signerName) u.signerName = String(signerName).trim();
|
|
2484
|
+
if (signerPosition) {
|
|
2485
|
+
u.signerPosition = String(signerPosition).trim();
|
|
2486
|
+
u.position = u.signerPosition;
|
|
2487
|
+
}
|
|
2488
|
+
if (bankName) u.bankName = String(bankName).trim();
|
|
2489
|
+
if (bik) u.bik = String(bik).trim();
|
|
2490
|
+
if (bankAccount) u.bankAccount = String(bankAccount).trim();
|
|
2491
|
+
if (corrAccount) u.corrAccount = String(corrAccount).trim();
|
|
2492
|
+
if (phone) u.phone = String(phone).trim();
|
|
2324
2493
|
}
|
|
2325
2494
|
}
|
|
2326
2495
|
this._save();
|