work-license-server 1.0.2 → 1.0.4
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/README.md +9 -2
- package/assets/admin.js +572 -0
- package/assets/landing.css +113 -1
- package/assets/landing.js +322 -290
- package/bin/server.mjs +5 -2
- package/package.json +1 -1
- package/src/admin.js +319 -0
- package/src/agent.js +152 -8
- package/src/bots.js +1 -1
- package/src/catalog.js +153 -17
- package/src/crypto.js +249 -17
- package/src/index.js +22 -1
- package/src/landing.js +113 -141
- package/src/server.js +229 -24
- package/src/store.js +696 -50
- package/src/yookassa.js +22 -8
package/assets/landing.js
CHANGED
|
@@ -15,11 +15,15 @@
|
|
|
15
15
|
let currentUser = null;
|
|
16
16
|
let currentStage = 'product';
|
|
17
17
|
let selectedCode = null;
|
|
18
|
+
let pendingAuthAction = null;
|
|
18
19
|
|
|
19
20
|
async function api(path, body, method) {
|
|
21
|
+
const headers = {};
|
|
22
|
+
if (body) headers['Content-Type'] = 'application/json';
|
|
23
|
+
if (currentUser?.sessionKey) headers['Authorization'] = 'Bearer ' + currentUser.sessionKey;
|
|
20
24
|
const res = await fetch(path, {
|
|
21
25
|
method: method || (body ? 'POST' : 'GET'),
|
|
22
|
-
headers:
|
|
26
|
+
headers: Object.keys(headers).length ? headers : undefined,
|
|
23
27
|
body: body ? JSON.stringify(body) : undefined,
|
|
24
28
|
});
|
|
25
29
|
let data = {};
|
|
@@ -29,6 +33,10 @@
|
|
|
29
33
|
return data;
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
function isUserReadyForOrders() {
|
|
37
|
+
return Boolean(currentUser && currentUser.ofertaAccepted);
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
/* ================= Тема ================= */
|
|
33
41
|
function applyTheme(theme) {
|
|
34
42
|
document.documentElement.setAttribute('data-theme', theme);
|
|
@@ -53,21 +61,20 @@
|
|
|
53
61
|
ind.style.width = active.offsetWidth + 'px';
|
|
54
62
|
}
|
|
55
63
|
function switchStage(id, { scroll = true } = {}) {
|
|
56
|
-
if (id === 'addons' && !
|
|
64
|
+
if (id === 'addons' && !isUserReadyForOrders()) {
|
|
65
|
+
requireAuthAndOferta(() => switchStage('addons'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
57
68
|
if (!$('#stage-' + id)) id = 'product';
|
|
58
69
|
currentStage = id;
|
|
70
|
+
document.body.setAttribute('data-stage', id);
|
|
59
71
|
$$('.tab').forEach(t => t.classList.toggle('active', t.dataset.stage === id));
|
|
60
72
|
$$('.stage').forEach(s => s.classList.toggle('active', s.id === 'stage-' + id));
|
|
61
|
-
const admBtn = $('#btnAdminTop');
|
|
62
|
-
if (admBtn) {
|
|
63
|
-
admBtn.classList.toggle('btn-primary', id === 'admin');
|
|
64
|
-
admBtn.classList.toggle('btn-ghost', id !== 'admin');
|
|
65
|
-
}
|
|
66
73
|
moveIndicator();
|
|
67
74
|
const active = $('.tab.active');
|
|
68
75
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'center', behavior: reduceMotion ? 'auto' : 'smooth' });
|
|
69
76
|
if (history.replaceState) {
|
|
70
|
-
const url = id === 'product' ? '/' :
|
|
77
|
+
const url = id === 'product' ? '/' : '/#' + id;
|
|
71
78
|
history.replaceState(null, '', url);
|
|
72
79
|
}
|
|
73
80
|
if (scroll) window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
|
|
@@ -75,7 +82,6 @@
|
|
|
75
82
|
observeReveals();
|
|
76
83
|
if (id === 'agents') playConsultantMock();
|
|
77
84
|
if (id === 'franchise') requestAnimationFrame(() => MAP.ready && applyView());
|
|
78
|
-
if (id === 'admin') refreshRegistry();
|
|
79
85
|
}
|
|
80
86
|
window.switchStage = switchStage;
|
|
81
87
|
|
|
@@ -123,46 +129,199 @@
|
|
|
123
129
|
card.style.setProperty('--my', (e.clientY - r.top) + 'px');
|
|
124
130
|
}, { passive: true });
|
|
125
131
|
|
|
126
|
-
/* ================= Авторизация ================= */
|
|
127
|
-
function openAuth(
|
|
128
|
-
|
|
132
|
+
/* ================= Авторизация и подписание публичной оферты (ПЭП) ================= */
|
|
133
|
+
function openAuth(hintMsg = '') {
|
|
134
|
+
if ($('#authErrorMsg')) $('#authErrorMsg').textContent = hintMsg || '';
|
|
135
|
+
if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = currentUser ? '' : 'none';
|
|
136
|
+
$('#authModal').classList.add('open');
|
|
137
|
+
setTimeout(() => $('#authEmail')?.focus(), 50);
|
|
138
|
+
}
|
|
139
|
+
function closeAuth() {
|
|
140
|
+
$('#authModal').classList.remove('open');
|
|
141
|
+
if ($('#authErrorMsg')) $('#authErrorMsg').textContent = '';
|
|
142
|
+
}
|
|
143
|
+
function openOfertaModal() {
|
|
144
|
+
$('#ofertaModal')?.classList.add('open');
|
|
145
|
+
}
|
|
146
|
+
function closeOfertaModal() {
|
|
147
|
+
$('#ofertaModal')?.classList.remove('open');
|
|
148
|
+
}
|
|
149
|
+
function requireAuthAndOferta(actionFn, prefill = {}) {
|
|
150
|
+
if (isUserReadyForOrders()) {
|
|
151
|
+
if (typeof actionFn === 'function') actionFn();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
pendingAuthAction = typeof actionFn === 'function' ? actionFn : null;
|
|
155
|
+
if (prefill.companyName && $('#authCompany')) $('#authCompany').value = prefill.companyName;
|
|
156
|
+
if (prefill.inn && $('#authInn')) $('#authInn').value = prefill.inn;
|
|
157
|
+
if (prefill.email && $('#authEmail')) $('#authEmail').value = prefill.email;
|
|
158
|
+
openAuth('Для оформления заказа, оплаты или резервирования франшизы зарегистрируйтесь и подпишите договор-оферту (ПЭП).');
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
129
162
|
async function performLogin() {
|
|
163
|
+
const errBox = $('#authErrorMsg');
|
|
164
|
+
if (errBox) errBox.textContent = '';
|
|
165
|
+
const email = $('#authEmail')?.value.trim() || '';
|
|
166
|
+
const companyName = $('#authCompany')?.value.trim() || '';
|
|
167
|
+
const inn = $('#authInn')?.value.trim() || '';
|
|
168
|
+
const signer = $('#authSigner')?.value.trim() || '';
|
|
169
|
+
const licenseId = $('#authLicId')?.value.trim() || '';
|
|
170
|
+
const ofertaChecked = Boolean($('#authOfertaCheck')?.checked);
|
|
171
|
+
|
|
172
|
+
if (!email || !email.includes('@')) {
|
|
173
|
+
if (errBox) errBox.textContent = 'Укажите корректный корпоративный email.';
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!companyName || !inn) {
|
|
177
|
+
if (errBox) errBox.textContent = 'Укажите наименование организации и ИНН.';
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!ofertaChecked) {
|
|
181
|
+
if (errBox) errBox.textContent = 'Для продолжения необходимо ознакомиться с условиями Публичной оферты и поставить отметку о подписании ПЭП.';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
130
185
|
try {
|
|
131
|
-
const data = await api('/api/auth/
|
|
132
|
-
email
|
|
133
|
-
companyName
|
|
134
|
-
|
|
186
|
+
const data = await api('/api/auth/register', {
|
|
187
|
+
email,
|
|
188
|
+
companyName,
|
|
189
|
+
inn,
|
|
190
|
+
name: signer || companyName,
|
|
191
|
+
licenseId,
|
|
192
|
+
ofertaAccepted: true,
|
|
135
193
|
});
|
|
136
194
|
currentUser = data;
|
|
137
195
|
try { localStorage.setItem('odant_work_session', data.sessionKey); } catch { /* приватный режим */ }
|
|
138
196
|
onAuthenticated();
|
|
139
197
|
closeAuth();
|
|
140
|
-
|
|
198
|
+
const nextAction = pendingAuthAction;
|
|
199
|
+
pendingAuthAction = null;
|
|
200
|
+
if (typeof nextAction === 'function') {
|
|
201
|
+
nextAction();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
switchStage('addons');
|
|
205
|
+
}
|
|
141
206
|
}
|
|
142
207
|
catch (err) {
|
|
143
|
-
|
|
208
|
+
if (errBox) errBox.textContent = err.message;
|
|
144
209
|
}
|
|
145
210
|
}
|
|
211
|
+
|
|
212
|
+
function performLogout() {
|
|
213
|
+
currentUser = null;
|
|
214
|
+
pendingAuthAction = null;
|
|
215
|
+
try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
|
|
216
|
+
$('#tileAddons').style.display = 'none';
|
|
217
|
+
$('#btnAuthText').textContent = 'Войти / Регистрация';
|
|
218
|
+
if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = 'none';
|
|
219
|
+
updateOfertaBanners();
|
|
220
|
+
updateClientSupportCard();
|
|
221
|
+
closeAuth();
|
|
222
|
+
if (currentStage === 'addons') switchStage('product');
|
|
223
|
+
moveIndicator();
|
|
224
|
+
}
|
|
225
|
+
|
|
146
226
|
async function restoreSession() {
|
|
147
227
|
let key = '';
|
|
148
228
|
try { key = localStorage.getItem('odant_work_session') || ''; } catch { /* приватный режим */ }
|
|
149
|
-
if (!key)
|
|
229
|
+
if (!key) {
|
|
230
|
+
updateOfertaBanners();
|
|
231
|
+
updateClientSupportCard();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
150
234
|
try {
|
|
151
235
|
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
|
|
152
|
-
if (data.authenticated) {
|
|
236
|
+
if (data.authenticated && data.user) {
|
|
153
237
|
currentUser = { ...data.user, sessionKey: key };
|
|
154
238
|
onAuthenticated();
|
|
155
|
-
if (location.hash === '#addons') switchStage('addons', { scroll: false });
|
|
239
|
+
if (location.hash === '#addons' && isUserReadyForOrders()) switchStage('addons', { scroll: false });
|
|
156
240
|
}
|
|
157
241
|
}
|
|
158
242
|
catch { /* сессия устарела */ }
|
|
243
|
+
updateOfertaBanners();
|
|
244
|
+
updateClientSupportCard();
|
|
159
245
|
}
|
|
246
|
+
|
|
247
|
+
function updateOfertaBanners() {
|
|
248
|
+
const ready = isUserReadyForOrders();
|
|
249
|
+
$$('[data-oferta-banner]').forEach(box => {
|
|
250
|
+
box.classList.toggle('signed', ready);
|
|
251
|
+
if (ready) {
|
|
252
|
+
box.innerHTML = `
|
|
253
|
+
<div>
|
|
254
|
+
<span class="tag ok">✓ Договор-оферта подписан ПЭП</span>
|
|
255
|
+
<div style="margin-top:6px;font-size:13.5px">
|
|
256
|
+
Организация: <b>${esc(currentUser.companyName)}</b> (ИНН ${esc(currentUser.inn || '—')}) · Подписант: <b>${esc(currentUser.ofertaSignerName || currentUser.name || '—')}</b> · Штамп ПЭП: <code>${esc(currentUser.ofertaSignatureId || 'PEP')}</code>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
260
|
+
<button class="btn btn-ghost btn-sm" data-action="open-oferta">Текст оферты</button>
|
|
261
|
+
<button class="btn btn-ghost btn-sm" data-action="auth">Профиль</button>
|
|
262
|
+
</div>`;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
box.innerHTML = `
|
|
266
|
+
<div>
|
|
267
|
+
<span class="tag">Требуется регистрация и акцепт оферты</span>
|
|
268
|
+
<div style="margin-top:6px;font-size:13.5px;color:var(--text-2)">
|
|
269
|
+
Любые действия по заказу комплекса, докупке лицензий, оплате через ЮKassa и резервированию региональной франшизы выполняются после регистрации и подписания Публичного договора-оферты (ПЭП).
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
273
|
+
<button class="btn btn-ghost btn-sm" data-action="open-oferta">Читать оферту</button>
|
|
274
|
+
<button class="btn btn-primary btn-sm" data-action="auth">Войти и подписать оферту</button>
|
|
275
|
+
</div>`;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function updateClientSupportCard() {
|
|
281
|
+
const box = $('#clientLicenseBox');
|
|
282
|
+
if (!box) return;
|
|
283
|
+
if (!currentUser || !currentUser.license) {
|
|
284
|
+
box.textContent = 'Войдите в личный кабинет и подпишите договор-оферту, чтобы получить код привязки вашего комплекса к ботам MAX, Telegram и VK.';
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const lic = currentUser.license;
|
|
288
|
+
const bindCode = lic.support?.bindCode || lic.licenseId;
|
|
289
|
+
box.textContent = [
|
|
290
|
+
`Организация: ${currentUser.companyName} (ИНН ${currentUser.inn || '—'})`,
|
|
291
|
+
`Оферта ПЭП: ${currentUser.ofertaAccepted ? ('подписана · ' + (currentUser.ofertaSignatureId || '')) : 'ожидает подписания'}`,
|
|
292
|
+
`Лицензия ПАК: ${lic.licenseId}`,
|
|
293
|
+
`Код привязки мессенджер-бота: ${bindCode}`,
|
|
294
|
+
`Команда для чата MAX / Telegram / VK: /bind ${bindCode}`,
|
|
295
|
+
`Рабочие места: ${lic.totalSeats} (${lic.baseSeats} вечных + ${lic.addonSeats} годовых)`,
|
|
296
|
+
`Уровень поддержки SLA: ${lic.support?.tierName || 'Enterprise'} (${lic.support?.reactionLabel || 'до 15 мин'})`,
|
|
297
|
+
`Остаток обращений: ${lic.support?.ticketsRemaining ?? 0} из ${lic.support?.ticketsQuota ?? 0}`,
|
|
298
|
+
].join('\n');
|
|
299
|
+
}
|
|
300
|
+
|
|
160
301
|
function onAuthenticated() {
|
|
161
302
|
if (!currentUser) return;
|
|
162
|
-
$('#tileAddons').style.display = '';
|
|
303
|
+
$('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
|
|
163
304
|
$('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
|
|
164
|
-
$('#inpLicId').value = currentUser.licenseId || '';
|
|
165
|
-
$('#
|
|
305
|
+
if ($('#inpLicId')) $('#inpLicId').value = currentUser.licenseId || '';
|
|
306
|
+
if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = currentUser.hubToken || '';
|
|
307
|
+
if ($('#authEmail')) $('#authEmail').value = currentUser.email || '';
|
|
308
|
+
if ($('#authCompany')) $('#authCompany').value = currentUser.companyName || '';
|
|
309
|
+
if ($('#authInn') && currentUser.inn) $('#authInn').value = currentUser.inn;
|
|
310
|
+
if ($('#authSigner') && (currentUser.ofertaSignerName || currentUser.name)) $('#authSigner').value = currentUser.ofertaSignerName || currentUser.name;
|
|
311
|
+
if ($('#authLicId')) $('#authLicId').value = currentUser.licenseId || '';
|
|
312
|
+
if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = Boolean(currentUser.ofertaAccepted);
|
|
313
|
+
if ($('#inpPakOrg') && currentUser.companyName) $('#inpPakOrg').value = currentUser.companyName;
|
|
314
|
+
if ($('#inpPakInn') && currentUser.inn) $('#inpPakInn').value = currentUser.inn;
|
|
315
|
+
if ($('#inpPakEmail') && currentUser.email) $('#inpPakEmail').value = currentUser.email;
|
|
316
|
+
if ($('#inpPartnerCompany') && currentUser.companyName) $('#inpPartnerCompany').value = currentUser.companyName;
|
|
317
|
+
if ($('#inpPartnerInn') && currentUser.inn) $('#inpPartnerInn').value = currentUser.inn;
|
|
318
|
+
if ($('#inpPartnerEmail') && currentUser.email) $('#inpPartnerEmail').value = currentUser.email;
|
|
319
|
+
if ($('#botText') && currentUser.license?.support?.bindCode && $('#botText').value === '/start') {
|
|
320
|
+
$('#botText').value = '/bind ' + currentUser.license.support.bindCode;
|
|
321
|
+
}
|
|
322
|
+
$('#userBadgeBox').innerHTML = `${esc(currentUser.companyName)} (ИНН ${esc(currentUser.inn || '—')}) · лицензия <b>${esc(currentUser.licenseId)}</b> · ПЭП <code>${esc(currentUser.ofertaSignatureId || '—')}</code>`;
|
|
323
|
+
updateOfertaBanners();
|
|
324
|
+
updateClientSupportCard();
|
|
166
325
|
moveIndicator();
|
|
167
326
|
recalcAddons();
|
|
168
327
|
}
|
|
@@ -203,7 +362,7 @@
|
|
|
203
362
|
['Мест × лет', `${seats} × ${years}`],
|
|
204
363
|
['Без скидки', rub(gross)],
|
|
205
364
|
['Скидка', `−${disc.discountPercent}%`],
|
|
206
|
-
['Всего мест после докупки', `${(D.product?.baseSeats ||
|
|
365
|
+
['Всего мест после докупки', `${(D.product?.baseSeats || 10) + seats}`],
|
|
207
366
|
]);
|
|
208
367
|
markLadder('ladderAddons', seats);
|
|
209
368
|
}
|
|
@@ -214,54 +373,97 @@
|
|
|
214
373
|
recalcAddons();
|
|
215
374
|
}
|
|
216
375
|
function recalcBaseOrder() {
|
|
376
|
+
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
377
|
+
const isLicenseOnly = variant === 'license_only';
|
|
378
|
+
const basePrice = isLicenseOnly ? (D.product.baseLicensePriceRub || 4_000_000) : D.product.basePriceRub;
|
|
217
379
|
const extra = Math.max(0, Number($('#inpPakExtraSeats').value) || 0);
|
|
218
380
|
const tier = tierById(segValue('segPakTier'));
|
|
219
381
|
const disc = volumeDiscount(extra);
|
|
220
382
|
const gross = extra * tier.pricePerUserYearRub;
|
|
221
383
|
const net = gross - Math.round(gross * disc.discountPercent / 100);
|
|
222
384
|
$('#lblPakSeats').textContent = extra;
|
|
223
|
-
$('#lblPakTotal').textContent = rub(
|
|
385
|
+
$('#lblPakTotal').textContent = rub(basePrice + net);
|
|
224
386
|
const rows = [
|
|
225
|
-
['Комплекс ODANT-WORK', rub(
|
|
387
|
+
[isLicenseOnly ? 'Базовая лицензия (без железа)' : 'Комплекс ODANT-WORK (с машиной)', rub(basePrice)],
|
|
226
388
|
['Вечные рабочие места', String(D.product.baseSeats)],
|
|
227
|
-
['Поддержка в комплекте', '1 год ·
|
|
389
|
+
['Поддержка в комплекте', '1 год · Enterprise (далее — по желанию)'],
|
|
228
390
|
];
|
|
229
391
|
if (extra > 0) rows.push([`Доп. места · ${esc(tier.name)}${disc.discountPercent ? ' · −' + disc.discountPercent + '%' : ''}`, `${extra} · ${rub(net)}`]);
|
|
230
392
|
$('#lblPakBreakdown').innerHTML = lines(rows);
|
|
231
393
|
}
|
|
232
|
-
async function submitBasePakOrder() {
|
|
394
|
+
async function submitBasePakOrder(paymentMethod = 'yookassa') {
|
|
395
|
+
if (!isUserReadyForOrders()) {
|
|
396
|
+
requireAuthAndOferta(() => submitBasePakOrder(paymentMethod), {
|
|
397
|
+
companyName: $('#inpPakOrg')?.value.trim(),
|
|
398
|
+
inn: $('#inpPakInn')?.value.trim(),
|
|
399
|
+
email: $('#inpPakEmail')?.value.trim(),
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
404
|
+
const isLicenseOnly = variant === 'license_only';
|
|
233
405
|
const st = $('#pakOrderStatus');
|
|
234
|
-
st.textContent = 'Формируем счёт ЮKassa…';
|
|
406
|
+
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
235
407
|
try {
|
|
236
408
|
const data = await api('/api/orders/checkout', {
|
|
237
|
-
|
|
409
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
410
|
+
includeBasePak: !isLicenseOnly,
|
|
411
|
+
includeBaseLicenseOnly: isLicenseOnly,
|
|
412
|
+
paymentMethod,
|
|
238
413
|
tier: segValue('segPakTier'),
|
|
239
414
|
addonSeats: Number($('#inpPakExtraSeats').value) || 0,
|
|
240
415
|
years: 1,
|
|
241
416
|
customer: { name: $('#inpPakOrg').value.trim(), inn: $('#inpPakInn').value.trim(), email: $('#inpPakEmail').value.trim() },
|
|
242
417
|
});
|
|
243
|
-
|
|
244
|
-
|
|
418
|
+
if (paymentMethod === 'invoice') {
|
|
419
|
+
st.innerHTML = `Счёт-договор <b>${esc(data.invoiceNumber || data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.invoiceUrl || data.confirmationUrl)}" target="_blank" rel="noopener">Открыть печатную форму счёта →</a>`;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
st.innerHTML = `Счёт <b>${esc(data.orderId)}</b> сформирован, лицензия <b>${esc(data.licenseId)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
|
|
423
|
+
}
|
|
424
|
+
refreshCatalogRegions();
|
|
245
425
|
}
|
|
246
426
|
catch (err) { st.textContent = err.message; }
|
|
247
427
|
}
|
|
248
|
-
async function submitAddonOrder() {
|
|
428
|
+
async function submitAddonOrder(paymentMethod = 'yookassa') {
|
|
429
|
+
if (!isUserReadyForOrders()) {
|
|
430
|
+
requireAuthAndOferta(() => submitAddonOrder(paymentMethod));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
249
433
|
const st = $('#orderStatus');
|
|
250
|
-
st.textContent = 'Формируем счёт ЮKassa…';
|
|
434
|
+
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
251
435
|
try {
|
|
252
436
|
const data = await api('/api/orders/checkout', {
|
|
437
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
253
438
|
includeBasePak: false,
|
|
439
|
+
paymentMethod,
|
|
254
440
|
tier: segValue('segTier'),
|
|
255
441
|
addonSeats: Number($('#inpSeats').value) || 1,
|
|
256
442
|
years: Number($('#inpYears').value) || 1,
|
|
257
443
|
licenseId: $('#inpLicId').value.trim(),
|
|
258
444
|
customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
|
|
259
445
|
});
|
|
260
|
-
|
|
261
|
-
|
|
446
|
+
if (paymentMethod === 'invoice') {
|
|
447
|
+
st.innerHTML = `Счёт-договор <b>${esc(data.invoiceNumber || data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.invoiceUrl || data.confirmationUrl)}" target="_blank" rel="noopener">Открыть счёт на оплату →</a>`;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
st.innerHTML = `Заказ <b>${esc(data.orderId)}</b> на <b>${rub(data.amountRub)}</b>. <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Перейти к оплате →</a>`;
|
|
451
|
+
}
|
|
452
|
+
refreshCatalogRegions();
|
|
262
453
|
}
|
|
263
454
|
catch (err) { st.textContent = err.message; }
|
|
264
455
|
}
|
|
456
|
+
async function generateCabinetToken() {
|
|
457
|
+
if (!currentUser?.sessionKey) return;
|
|
458
|
+
try {
|
|
459
|
+
const res = await api('/api/cabinet/token', { sessionKey: currentUser.sessionKey });
|
|
460
|
+
if (res?.token) {
|
|
461
|
+
currentUser.hubToken = res.token;
|
|
462
|
+
if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = res.token;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch (err) { alert(err.message); }
|
|
466
|
+
}
|
|
265
467
|
|
|
266
468
|
function recalcPartner() {
|
|
267
469
|
const m = Number($('#rngPartnerSales').value) || 0;
|
|
@@ -640,10 +842,19 @@
|
|
|
640
842
|
code = code || selectedCode;
|
|
641
843
|
if (!code) return;
|
|
642
844
|
selectRegion(code);
|
|
845
|
+
if (!isUserReadyForOrders()) {
|
|
846
|
+
requireAuthAndOferta(() => reserveRegionFranchise(code), {
|
|
847
|
+
companyName: $('#inpPartnerCompany')?.value.trim(),
|
|
848
|
+
inn: $('#inpPartnerInn')?.value.trim(),
|
|
849
|
+
email: $('#inpPartnerEmail')?.value.trim(),
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
643
853
|
const msg = $('#partnerActionMsg');
|
|
644
854
|
msg.textContent = 'Оформляем резерв и счёт на залог…';
|
|
645
855
|
try {
|
|
646
856
|
const data = await api('/api/partners/reserve', {
|
|
857
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
647
858
|
regionCode: code,
|
|
648
859
|
companyName: $('#inpPartnerCompany').value.trim(),
|
|
649
860
|
inn: $('#inpPartnerInn').value.trim(),
|
|
@@ -654,245 +865,47 @@
|
|
|
654
865
|
renderRegionsTable();
|
|
655
866
|
selectRegion(code);
|
|
656
867
|
msg.innerHTML = `Квота в субъекте «${esc(data.reservation.regionName)}» закреплена за вами (резерв <b>${esc(data.reservation.reservationId)}</b>). <a href="${esc(data.confirmationUrl)}" target="_blank" rel="noopener">Оплатить залог ${rub(data.amountRub)} →</a>`;
|
|
657
|
-
|
|
868
|
+
refreshCatalogRegions();
|
|
658
869
|
}
|
|
659
870
|
catch (err) { msg.textContent = err.message; }
|
|
660
871
|
}
|
|
661
872
|
window.reserveRegionFranchise = reserveRegionFranchise;
|
|
662
873
|
|
|
663
|
-
/* ================= Поддержка ================= */
|
|
874
|
+
/* ================= Поддержка и синхронизация квот ================= */
|
|
664
875
|
async function sendBot() {
|
|
665
876
|
const box = $('#botReplyBox');
|
|
666
877
|
try {
|
|
667
878
|
const data = await api('/api/bots/' + $('#botChannel').value + '/webhook', {
|
|
668
|
-
chat_id: $('#botChatId').value.trim(),
|
|
879
|
+
chat_id: $('#botChatId').value.trim(),
|
|
880
|
+
text: $('#botText').value,
|
|
881
|
+
user_name: currentUser?.companyName || 'Клиент ODANT-WORK',
|
|
669
882
|
});
|
|
670
883
|
box.textContent = data.reply || data.result?.reply || JSON.stringify(data, null, 2);
|
|
671
884
|
}
|
|
672
885
|
catch (err) { box.textContent = err.message; }
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
let lastAdminData = null;
|
|
684
|
-
|
|
685
|
-
async function refreshRegistry() {
|
|
686
|
-
let data;
|
|
687
|
-
try { data = await api('/api/admin/overview'); }
|
|
688
|
-
catch { return; }
|
|
689
|
-
lastAdminData = data;
|
|
690
|
-
if (Array.isArray(data.regions) && data.regions.length) {
|
|
691
|
-
data.regions.forEach(mergeRegion);
|
|
692
|
-
paintMap();
|
|
693
|
-
renderRegionsTable();
|
|
694
|
-
if (selectedCode) selectRegion(selectedCode);
|
|
695
|
-
}
|
|
696
|
-
const out = [];
|
|
697
|
-
out.push(`Лицензии (${data.licenses?.length || 0})`);
|
|
698
|
-
for (const l of data.licenses || [])
|
|
699
|
-
out.push(` ${l.licenseId} · ${l.customer?.name || '—'} · мест ${l.totalSeats} (${l.baseSeats ?? 5} вечных + ${l.addonSeats}) · ${l.support?.tierName || ''}`);
|
|
700
|
-
out.push('', `Резервы франшиз (${data.reservations?.length || 0})`);
|
|
701
|
-
for (const r of data.reservations || [])
|
|
702
|
-
out.push(` ${r.reservationId} · ${r.regionName} · ${r.companyName} · залог ${r.depositPaid ? 'оплачен' : 'ожидает оплаты'}`);
|
|
703
|
-
out.push('', `Обращения (${data.tickets?.length || 0})`);
|
|
704
|
-
for (const t of data.tickets || [])
|
|
705
|
-
out.push(` ${t.ticketId} [${String(t.channel).toUpperCase()}] ${t.tierName}: ${t.messages?.[0]?.text || ''}`);
|
|
706
|
-
if ($('#registryBox')) $('#registryBox').textContent = out.join('\n');
|
|
707
|
-
renderAdminPanel(data);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function fmtDate(ms) {
|
|
711
|
-
if (!ms) return '—';
|
|
712
|
-
try { return new Date(ms).toLocaleDateString('ru-RU'); } catch { return '—'; }
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function renderAdminPanel(data = {}) {
|
|
716
|
-
const licenses = data.licenses || [];
|
|
717
|
-
const orders = data.orders || [];
|
|
718
|
-
const partners = data.partners || [];
|
|
719
|
-
const reservations = data.reservations || [];
|
|
720
|
-
const tickets = data.tickets || [];
|
|
721
|
-
|
|
722
|
-
const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
|
|
723
|
-
const paidOrders = orders.filter(o => o.status === 'paid');
|
|
724
|
-
const pendingOrders = orders.filter(o => o.status === 'pending');
|
|
725
|
-
const paidRub = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
726
|
-
const pendingRub = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
727
|
-
const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
|
|
728
|
-
|
|
729
|
-
if ($('#admStatLic')) {
|
|
730
|
-
$('#admStatLic').textContent = String(licenses.length);
|
|
731
|
-
$('#admStatLicSub').textContent = `лицензий ПАК · ${totalSeats} активных мест`;
|
|
732
|
-
$('#admStatPaid').textContent = rub(paidRub);
|
|
733
|
-
$('#admStatPaidSub').textContent = `оплачено заказов: ${paidOrders.length}`;
|
|
734
|
-
$('#admStatPending').textContent = rub(pendingRub);
|
|
735
|
-
$('#admStatPendingSub').textContent = `ожидают оплаты: ${pendingOrders.length}`;
|
|
736
|
-
$('#admStatPartners').textContent = `${partners.length} / ${reservations.length}`;
|
|
737
|
-
$('#admStatTickets').textContent = String(openTickets.length);
|
|
738
|
-
$('#admStatTicketsSub').textContent = `открытых из ${tickets.length} обращений`;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if ($('#admLicensesBody')) {
|
|
742
|
-
$('#admLicensesBody').innerHTML = licenses.map(l => `<tr>
|
|
743
|
-
<td><b>${esc(l.licenseId)}</b><div class="small muted">Код бота: <code>${esc(l.bindingCode || l.licenseId)}</code></div></td>
|
|
744
|
-
<td><b>${esc(l.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(l.customer?.inn || '—')} · ${esc(l.customer?.email || '—')}</div></td>
|
|
745
|
-
<td><code>${esc(l.boundHwid || 'не привязан')}</code><div class="small muted">${esc((l.boundNodeFingerprint || '').slice(0, 18) || 'ожидает синхронизации')}</div></td>
|
|
746
|
-
<td class="num"><b>${l.totalSeats}</b> <span class="muted">(${l.baseSeats ?? 5} вечн. + ${l.addonSeats || 0} доп.)</span></td>
|
|
747
|
-
<td><span class="tag ok">${esc(l.support?.tierName || 'Business')}</span><div class="small muted">до ${fmtDate(l.support?.expiresAt)}</div></td>
|
|
748
|
-
<td><button class="btn btn-ghost btn-sm" data-cert="${esc(l.licenseId)}">Сертификат JSON</button></td>
|
|
749
|
-
</tr>`).join('') || '<tr><td colspan="6" class="muted">Лицензий пока нет — нажмите «+ Демо-лицензия ПАК»</td></tr>';
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if ($('#admOrdersBody')) {
|
|
753
|
-
$('#admOrdersBody').innerHTML = orders.map(o => {
|
|
754
|
-
const stTag = o.status === 'paid' ? 'ok' : (o.status === 'canceled' ? 'bad' : '');
|
|
755
|
-
const stLabel = o.status === 'paid' ? 'оплачен' : (o.status === 'canceled' ? 'отменён' : 'ожидает оплаты');
|
|
756
|
-
return `<tr>
|
|
757
|
-
<td><b>${esc(o.orderId)}</b><div class="small muted">${esc(o.paymentId || '—')}</div></td>
|
|
758
|
-
<td><b>${esc(o.tierName || o.kind || 'Заказ')}</b><div class="small muted">${o.includeBasePak ? 'Комплекс ПАК' : (o.kind === 'franchise_reservation' ? 'Залог франшизы' : `Доп. места: ${o.addonSeats || 0} × ${o.years || 1} г.`)}</div></td>
|
|
759
|
-
<td><b>${esc(o.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(o.customer?.inn || '—')}</div></td>
|
|
760
|
-
<td class="num"><b>${rub(o.amountRub)}</b>${o.volumeDiscountPercent ? `<div class="small" style="color:var(--ok)">скидка −${o.volumeDiscountPercent}%</div>` : ''}</td>
|
|
761
|
-
<td><span class="tag ${stTag}">${stLabel}</span></td>
|
|
762
|
-
<td><div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
763
|
-
${o.status === 'pending' ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(o.orderId)}">Провести оплату</button>` : ''}
|
|
764
|
-
<a class="btn btn-ghost btn-sm" href="/pay/sandbox/${encodeURIComponent(o.orderId)}" target="_blank" rel="noopener">Песочница</a>
|
|
765
|
-
</div></td>
|
|
766
|
-
</tr>`;
|
|
767
|
-
}).join('') || '<tr><td colspan="6" class="muted">Заказов пока нет</td></tr>';
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if ($('#admPartnersBody')) {
|
|
771
|
-
const resRows = reservations.map(r => `<tr>
|
|
772
|
-
<td><b>${esc(r.reservationId)}</b><div class="small muted">Резерв франшизы</div></td>
|
|
773
|
-
<td><b>${esc(r.regionName)}</b> <span class="muted">(${esc(r.regionCode)})</span></td>
|
|
774
|
-
<td><b>${esc(r.companyName)}</b><div class="small muted">ИНН ${esc(r.inn || '—')} · ${esc(r.email || '—')}</div></td>
|
|
775
|
-
<td>Паушальный взнос <b>${rub(r.franchiseFeeRub)}</b><div class="small muted">Залог ${rub(r.nonRefundableDepositRub || r.depositAmountRub || D.franchise.reservationDepositRub)} · полная оплата за 1 год</div></td>
|
|
776
|
-
<td><span class="tag ${r.depositPaid ? 'ok' : ''}">${r.depositPaid ? 'залог оплачен' : 'ждёт оплаты залога'}</span></td>
|
|
777
|
-
<td><div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
778
|
-
${!r.depositPaid && r.orderId ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(r.orderId)}">Оплатить залог</button>` : ''}
|
|
779
|
-
${!r.convertedToPartnerId ? `<button class="btn btn-primary btn-sm" data-activate-res="${esc(r.reservationId)}">Активировать партнёра</button>` : `<span class="small muted">Партнёр ${esc(r.convertedToPartnerId)}</span>`}
|
|
780
|
-
</div></td>
|
|
781
|
-
</tr>`);
|
|
782
|
-
const partRows = partners.map(p => `<tr>
|
|
783
|
-
<td><b>${esc(p.partnerId)}</b><div class="small muted">Действующий партнёр</div></td>
|
|
784
|
-
<td><b>${esc(p.regionName)}</b> <span class="muted">(${esc(p.regionCode)})</span></td>
|
|
785
|
-
<td><b>${esc(p.companyName)}</b><div class="small muted">ИНН ${esc(p.inn || '—')}</div></td>
|
|
786
|
-
<td>Скидка <b>${p.discountPercent}%</b> · ${esc(p.statusLevel)}<div class="small muted">Продажи: ${rub(p.annualSalesRub || 0)}</div></td>
|
|
787
|
-
<td><span class="tag ok">сертификатов: ${(p.certifiedSpecialists || []).length}</span></td>
|
|
788
|
-
<td><button class="btn btn-ghost btn-sm" data-certify-partner="${esc(p.partnerId)}">+ Аттестовать инженера</button></td>
|
|
789
|
-
</tr>`);
|
|
790
|
-
$('#admPartnersBody').innerHTML = [...resRows, ...partRows].join('') || '<tr><td colspan="6" class="muted">Резервов и партнёров пока нет — выберите субъект во вкладке «Франшиза»</td></tr>';
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
if ($('#admTicketsBody')) {
|
|
794
|
-
$('#admTicketsBody').innerHTML = tickets.map(t => {
|
|
795
|
-
const msgs = (t.messages || []).map(m => `<div class="small"><b>${esc(m.role === 'engineer' ? (m.author || 'Инженер') : (t.userName || 'Клиент'))}:</b> ${esc(m.text)}</div>`).join('');
|
|
796
|
-
return `<tr>
|
|
797
|
-
<td><b>${esc(t.ticketId)}</b><div><span class="tag">${esc(String(t.channel || '').toUpperCase())}</span></div></td>
|
|
798
|
-
<td><b>${esc(t.userName || t.chatId)}</b><div class="small muted">Лицензия: ${esc(t.licenseId || 'не привязана')}</div></td>
|
|
799
|
-
<td><span class="tag ok">${esc(t.tierName || 'Start')}</span><div class="small muted">реакция ≤ ${t.slaReactionMinutes || 480} мин</div></td>
|
|
800
|
-
<td style="white-space:normal;max-width:360px">${msgs}</td>
|
|
801
|
-
<td>
|
|
802
|
-
<div style="display:flex;gap:6px">
|
|
803
|
-
<input type="text" id="reply-${esc(t.ticketId)}" placeholder="Ответ инженера…" style="min-width:190px;padding:7px 10px;font-size:12.5px">
|
|
804
|
-
<button class="btn btn-primary btn-sm" data-reply-ticket="${esc(t.ticketId)}">Ответить</button>
|
|
805
|
-
</div>
|
|
806
|
-
</td>
|
|
807
|
-
</tr>`;
|
|
808
|
-
}).join('') || '<tr><td colspan="5" class="muted">Обращений пока нет — отправьте сообщение через «Консоль бота» во вкладке «Поддержка»</td></tr>';
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
async function adminIssueLicense() {
|
|
813
|
-
const st = $('#admIssueStatus');
|
|
814
|
-
st.textContent = 'Выпускаем сертификат…';
|
|
815
|
-
try {
|
|
816
|
-
const lic = await api('/api/license/issue', {
|
|
817
|
-
hwid: $('#admLicHwid').value.trim(),
|
|
818
|
-
customer: {
|
|
819
|
-
name: $('#admLicOrg').value.trim(),
|
|
820
|
-
inn: $('#admLicInn').value.trim(),
|
|
821
|
-
email: $('#admLicEmail').value.trim(),
|
|
822
|
-
},
|
|
823
|
-
});
|
|
824
|
-
st.textContent = `Выпущена лицензия ${lic.licenseId} (${lic.totalSeats} мест, подпись Ed25519)`;
|
|
825
|
-
refreshRegistry();
|
|
826
|
-
}
|
|
827
|
-
catch (err) { st.textContent = err.message; }
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
async function adminConfirmOrder(orderId) {
|
|
831
|
-
const order = (lastAdminData?.orders || []).find(o => o.orderId === orderId);
|
|
832
|
-
if (!order) return;
|
|
833
|
-
await api('/api/payments/yookassa/webhook', {
|
|
834
|
-
type: 'notification',
|
|
835
|
-
event: 'payment.succeeded',
|
|
836
|
-
object: {
|
|
837
|
-
id: order.paymentId,
|
|
838
|
-
status: 'succeeded',
|
|
839
|
-
amount: { value: Number(order.amountRub || 0).toFixed(2), currency: 'RUB' },
|
|
840
|
-
metadata: { orderId: order.orderId, licenseId: order.licenseId || '', reservationId: order.reservationId || '' },
|
|
841
|
-
},
|
|
842
|
-
});
|
|
843
|
-
refreshRegistry();
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
async function adminActivateReservation(reservationId) {
|
|
847
|
-
const r = (lastAdminData?.reservations || []).find(x => x.reservationId === reservationId);
|
|
848
|
-
if (!r) return;
|
|
849
|
-
try {
|
|
850
|
-
await api('/api/partners/register', {
|
|
851
|
-
reservationId: r.reservationId,
|
|
852
|
-
regionCode: r.regionCode,
|
|
853
|
-
companyName: r.companyName,
|
|
854
|
-
inn: r.inn,
|
|
855
|
-
email: r.email,
|
|
856
|
-
franchiseFeePaid: true,
|
|
857
|
-
});
|
|
858
|
-
refreshRegistry();
|
|
886
|
+
if (currentUser?.sessionKey) {
|
|
887
|
+
try {
|
|
888
|
+
const me = await api('/api/auth/me?sessionKey=' + encodeURIComponent(currentUser.sessionKey));
|
|
889
|
+
if (me.authenticated && me.user) {
|
|
890
|
+
currentUser = { ...me.user, sessionKey: currentUser.sessionKey };
|
|
891
|
+
updateClientSupportCard();
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
catch { /* ignore */ }
|
|
859
895
|
}
|
|
860
|
-
catch (err) { alert(err.message); }
|
|
861
896
|
}
|
|
862
897
|
|
|
863
|
-
async function
|
|
898
|
+
async function refreshCatalogRegions() {
|
|
864
899
|
try {
|
|
865
|
-
await api('/api/
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
refreshRegistry();
|
|
900
|
+
const data = await api('/api/catalog');
|
|
901
|
+
if (Array.isArray(data.regions) && data.regions.length) {
|
|
902
|
+
data.regions.forEach(mergeRegion);
|
|
903
|
+
paintMap();
|
|
904
|
+
renderRegionsTable();
|
|
905
|
+
if (selectedCode) selectRegion(selectedCode);
|
|
906
|
+
}
|
|
873
907
|
}
|
|
874
|
-
catch
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async function adminReplyTicket(ticketId) {
|
|
878
|
-
const inp = document.getElementById('reply-' + ticketId);
|
|
879
|
-
const text = inp?.value.trim();
|
|
880
|
-
if (!text) return;
|
|
881
|
-
await api('/api/tickets/' + encodeURIComponent(ticketId) + '/reply', {
|
|
882
|
-
engineerName: 'Дежурный инженер ODANT',
|
|
883
|
-
text,
|
|
884
|
-
});
|
|
885
|
-
inp.value = '';
|
|
886
|
-
refreshRegistry();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function openCertModal(licenseId) {
|
|
890
|
-
const lic = (lastAdminData?.licenses || []).find(l => l.licenseId === licenseId);
|
|
891
|
-
if (!lic) return;
|
|
892
|
-
$('#certTitle').textContent = `Сертификат ${lic.licenseId}`;
|
|
893
|
-
$('#certSub').textContent = `${lic.customer?.name || ''} · Ed25519 (WORK-LIC-v1)`;
|
|
894
|
-
$('#certJsonBox').textContent = JSON.stringify(lic.certificate || lic, null, 2);
|
|
895
|
-
$('#certModal').classList.add('open');
|
|
908
|
+
catch { /* ignore */ }
|
|
896
909
|
}
|
|
897
910
|
|
|
898
911
|
/* ================= ИИ-агент лицензионного центра ================= */
|
|
@@ -903,7 +916,10 @@
|
|
|
903
916
|
if (open) setTimeout(() => $('#aiInput').focus(), 60);
|
|
904
917
|
}
|
|
905
918
|
function md(text) {
|
|
906
|
-
return esc(text)
|
|
919
|
+
return esc(text)
|
|
920
|
+
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
|
|
921
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
922
|
+
.replace(/(^|\s)\*(.+?)\*(?=\s|$)/g, '$1<i>$2</i>');
|
|
907
923
|
}
|
|
908
924
|
async function sendAi(text) {
|
|
909
925
|
const inp = $('#aiInput');
|
|
@@ -924,14 +940,20 @@
|
|
|
924
940
|
switchStage('franchise');
|
|
925
941
|
setTimeout(() => selectRegion(act.regionCode, { zoom: true, scrollTable: true }), 80);
|
|
926
942
|
}
|
|
927
|
-
else if (act.type === 'prefill_calculator' &&
|
|
943
|
+
else if (act.type === 'prefill_calculator' && isUserReadyForOrders()) {
|
|
928
944
|
$$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === act.tier));
|
|
929
945
|
if (act.years) $('#inpYears').value = act.years;
|
|
930
946
|
syncSeats(act.seats);
|
|
931
947
|
switchStage('addons');
|
|
932
948
|
}
|
|
933
|
-
else if (act.type === '
|
|
934
|
-
|
|
949
|
+
else if (act.type === 'open_auth') {
|
|
950
|
+
openAuth();
|
|
951
|
+
}
|
|
952
|
+
else if (act.type === 'open_oferta') {
|
|
953
|
+
openOfertaModal();
|
|
954
|
+
}
|
|
955
|
+
else if (act.type === 'switch_stage' && act.stage && act.stage !== 'admin') {
|
|
956
|
+
switchStage(act.stage === 'addons' && !isUserReadyForOrders() ? 'pricing' : act.stage);
|
|
935
957
|
}
|
|
936
958
|
}
|
|
937
959
|
}
|
|
@@ -1101,14 +1123,9 @@
|
|
|
1101
1123
|
/* ================= Привязка событий ================= */
|
|
1102
1124
|
function bind() {
|
|
1103
1125
|
document.addEventListener('click', (e) => {
|
|
1104
|
-
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],
|
|
1126
|
+
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
|
|
1105
1127
|
if (!el) return;
|
|
1106
1128
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1107
|
-
if (el.matches('[data-cert]')) { openCertModal(el.dataset.cert); return; }
|
|
1108
|
-
if (el.matches('[data-pay-order]')) { adminConfirmOrder(el.dataset.payOrder); return; }
|
|
1109
|
-
if (el.matches('[data-activate-res]')) { adminActivateReservation(el.dataset.activateRes); return; }
|
|
1110
|
-
if (el.matches('[data-certify-partner]')) { adminCertifyPartner(el.dataset.certifyPartner); return; }
|
|
1111
|
-
if (el.matches('[data-reply-ticket]')) { adminReplyTicket(el.dataset.replyTicket); return; }
|
|
1112
1129
|
if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
|
|
1113
1130
|
if (el.matches('th[data-sort]')) {
|
|
1114
1131
|
SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
|
|
@@ -1121,22 +1138,35 @@
|
|
|
1121
1138
|
if (el.matches('[data-ask]')) { sendAi(el.dataset.ask); return; }
|
|
1122
1139
|
if (el.dataset.stage) { e.preventDefault(); switchStage(el.dataset.stage); return; }
|
|
1123
1140
|
switch (el.dataset.action) {
|
|
1124
|
-
case 'auth':
|
|
1141
|
+
case 'auth': openAuth(); break;
|
|
1125
1142
|
case 'auth-close': closeAuth(); break;
|
|
1126
1143
|
case 'login': performLogin(); break;
|
|
1127
|
-
case '
|
|
1144
|
+
case 'logout': performLogout(); break;
|
|
1145
|
+
case 'open-oferta': e.preventDefault(); openOfertaModal(); break;
|
|
1146
|
+
case 'oferta-close': closeOfertaModal(); break;
|
|
1147
|
+
case 'accept-from-oferta':
|
|
1148
|
+
closeOfertaModal();
|
|
1149
|
+
if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = true;
|
|
1150
|
+
openAuth();
|
|
1151
|
+
break;
|
|
1152
|
+
case 'addons':
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
if (isUserReadyForOrders()) switchStage('addons');
|
|
1155
|
+
else requireAuthAndOferta(() => switchStage('addons'));
|
|
1156
|
+
break;
|
|
1128
1157
|
case 'pick-tier':
|
|
1129
1158
|
$$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
|
|
1130
1159
|
recalcAddons();
|
|
1131
|
-
if (
|
|
1160
|
+
if (isUserReadyForOrders()) switchStage('addons');
|
|
1161
|
+
else requireAuthAndOferta(() => switchStage('addons'));
|
|
1132
1162
|
break;
|
|
1133
|
-
case 'order-pak': submitBasePakOrder(); break;
|
|
1134
|
-
case 'order-
|
|
1163
|
+
case 'order-pak': submitBasePakOrder('yookassa'); break;
|
|
1164
|
+
case 'order-pak-invoice': submitBasePakOrder('invoice'); break;
|
|
1165
|
+
case 'order-addons': submitAddonOrder('yookassa'); break;
|
|
1166
|
+
case 'order-addons-invoice': submitAddonOrder('invoice'); break;
|
|
1167
|
+
case 'gen-hub-token': generateCabinetToken(); break;
|
|
1135
1168
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
1136
1169
|
case 'bot-send': sendBot(); break;
|
|
1137
|
-
case 'registry': refreshRegistry(); break;
|
|
1138
|
-
case 'demo-pak': createDemoPak(); break;
|
|
1139
|
-
case 'adm-issue-lic': adminIssueLicense(); break;
|
|
1140
1170
|
case 'cert-close': $('#certModal')?.classList.remove('open'); break;
|
|
1141
1171
|
case 'cert-copy':
|
|
1142
1172
|
navigator.clipboard?.writeText($('#certJsonBox')?.textContent || '');
|
|
@@ -1145,22 +1175,24 @@
|
|
|
1145
1175
|
case 'ai-send': sendAi(); break;
|
|
1146
1176
|
}
|
|
1147
1177
|
});
|
|
1148
|
-
$('#authModal')
|
|
1178
|
+
$('#authModal')?.addEventListener('click', (e) => { if (e.target.id === 'authModal') closeAuth(); });
|
|
1179
|
+
$('#ofertaModal')?.addEventListener('click', (e) => { if (e.target.id === 'ofertaModal') closeOfertaModal(); });
|
|
1149
1180
|
$('#certModal')?.addEventListener('click', (e) => { if (e.target.id === 'certModal') $('#certModal').classList.remove('open'); });
|
|
1150
1181
|
document.addEventListener('keydown', (e) => {
|
|
1151
|
-
if (e.key === 'Escape') { closeAuth(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
|
|
1182
|
+
if (e.key === 'Escape') { closeAuth(); closeOfertaModal(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
|
|
1152
1183
|
});
|
|
1153
|
-
$('#authModal')
|
|
1154
|
-
$('#aiInput')
|
|
1155
|
-
$('#botText')
|
|
1156
|
-
$('#rngAddonSeats')
|
|
1157
|
-
$('#inpSeats')
|
|
1158
|
-
$('#inpYears')
|
|
1159
|
-
$('#inpPakExtraSeats')
|
|
1160
|
-
$('#rngPartnerSales')
|
|
1161
|
-
$('#inpRegionFilter')
|
|
1184
|
+
$('#authModal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') performLogin(); });
|
|
1185
|
+
$('#aiInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
|
|
1186
|
+
$('#botText')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendBot(); });
|
|
1187
|
+
$('#rngAddonSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
|
|
1188
|
+
$('#inpSeats')?.addEventListener('input', (e) => syncSeats(e.target.value));
|
|
1189
|
+
$('#inpYears')?.addEventListener('input', recalcAddons);
|
|
1190
|
+
$('#inpPakExtraSeats')?.addEventListener('input', recalcBaseOrder);
|
|
1191
|
+
$('#rngPartnerSales')?.addEventListener('input', recalcPartner);
|
|
1192
|
+
$('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
|
|
1162
1193
|
bindSeg('segTier', recalcAddons);
|
|
1163
1194
|
bindSeg('segPakTier', recalcBaseOrder);
|
|
1195
|
+
bindSeg('segPakVariant', recalcBaseOrder);
|
|
1164
1196
|
window.addEventListener('resize', moveIndicator);
|
|
1165
1197
|
window.addEventListener('hashchange', () => { const id = location.hash.slice(1); if (id && id !== currentStage) switchStage(id, { scroll: false }); });
|
|
1166
1198
|
}
|
|
@@ -1174,12 +1206,12 @@
|
|
|
1174
1206
|
recalcPartner();
|
|
1175
1207
|
renderRegionsTable();
|
|
1176
1208
|
initBackdrop();
|
|
1177
|
-
const initial = D.initialStage ||
|
|
1178
|
-
switchStage(initial && initial !== 'addons' ? initial : 'product', { scroll: false });
|
|
1209
|
+
const initial = D.initialStage || location.hash.slice(1);
|
|
1210
|
+
switchStage(initial && initial !== 'addons' && initial !== 'admin' ? initial : 'product', { scroll: false });
|
|
1179
1211
|
if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
|
|
1180
1212
|
initMap().then(() => selectRegion(selectedCode || 'RU-SVE'));
|
|
1181
1213
|
restoreSession();
|
|
1182
|
-
|
|
1214
|
+
refreshCatalogRegions();
|
|
1183
1215
|
}
|
|
1184
1216
|
|
|
1185
1217
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|