work-license-server 1.0.3 → 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 +8 -2
- package/assets/admin.js +572 -0
- package/assets/landing.css +72 -1
- package/assets/landing.js +315 -332
- package/bin/server.mjs +5 -2
- package/package.json +1 -1
- package/src/admin.js +319 -0
- package/src/agent.js +71 -5
- package/src/catalog.js +141 -8
- package/src/crypto.js +249 -17
- package/src/index.js +22 -1
- package/src/landing.js +94 -171
- package/src/server.js +229 -24
- package/src/store.js +693 -47
- 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,22 +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;
|
|
59
70
|
document.body.setAttribute('data-stage', id);
|
|
60
71
|
$$('.tab').forEach(t => t.classList.toggle('active', t.dataset.stage === id));
|
|
61
72
|
$$('.stage').forEach(s => s.classList.toggle('active', s.id === 'stage-' + id));
|
|
62
|
-
const admBtn = $('#btnAdminTop');
|
|
63
|
-
if (admBtn) {
|
|
64
|
-
admBtn.classList.toggle('btn-primary', id === 'admin');
|
|
65
|
-
admBtn.classList.toggle('btn-ghost', id !== 'admin');
|
|
66
|
-
}
|
|
67
73
|
moveIndicator();
|
|
68
74
|
const active = $('.tab.active');
|
|
69
75
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'center', behavior: reduceMotion ? 'auto' : 'smooth' });
|
|
70
76
|
if (history.replaceState) {
|
|
71
|
-
const url = id === 'product' ? '/' :
|
|
77
|
+
const url = id === 'product' ? '/' : '/#' + id;
|
|
72
78
|
history.replaceState(null, '', url);
|
|
73
79
|
}
|
|
74
80
|
if (scroll) window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
|
|
@@ -76,7 +82,6 @@
|
|
|
76
82
|
observeReveals();
|
|
77
83
|
if (id === 'agents') playConsultantMock();
|
|
78
84
|
if (id === 'franchise') requestAnimationFrame(() => MAP.ready && applyView());
|
|
79
|
-
if (id === 'admin') refreshRegistry();
|
|
80
85
|
}
|
|
81
86
|
window.switchStage = switchStage;
|
|
82
87
|
|
|
@@ -124,46 +129,199 @@
|
|
|
124
129
|
card.style.setProperty('--my', (e.clientY - r.top) + 'px');
|
|
125
130
|
}, { passive: true });
|
|
126
131
|
|
|
127
|
-
/* ================= Авторизация ================= */
|
|
128
|
-
function openAuth(
|
|
129
|
-
|
|
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
|
+
|
|
130
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
|
+
|
|
131
185
|
try {
|
|
132
|
-
const data = await api('/api/auth/
|
|
133
|
-
email
|
|
134
|
-
companyName
|
|
135
|
-
|
|
186
|
+
const data = await api('/api/auth/register', {
|
|
187
|
+
email,
|
|
188
|
+
companyName,
|
|
189
|
+
inn,
|
|
190
|
+
name: signer || companyName,
|
|
191
|
+
licenseId,
|
|
192
|
+
ofertaAccepted: true,
|
|
136
193
|
});
|
|
137
194
|
currentUser = data;
|
|
138
195
|
try { localStorage.setItem('odant_work_session', data.sessionKey); } catch { /* приватный режим */ }
|
|
139
196
|
onAuthenticated();
|
|
140
197
|
closeAuth();
|
|
141
|
-
|
|
198
|
+
const nextAction = pendingAuthAction;
|
|
199
|
+
pendingAuthAction = null;
|
|
200
|
+
if (typeof nextAction === 'function') {
|
|
201
|
+
nextAction();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
switchStage('addons');
|
|
205
|
+
}
|
|
142
206
|
}
|
|
143
207
|
catch (err) {
|
|
144
|
-
|
|
208
|
+
if (errBox) errBox.textContent = err.message;
|
|
145
209
|
}
|
|
146
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
|
+
|
|
147
226
|
async function restoreSession() {
|
|
148
227
|
let key = '';
|
|
149
228
|
try { key = localStorage.getItem('odant_work_session') || ''; } catch { /* приватный режим */ }
|
|
150
|
-
if (!key)
|
|
229
|
+
if (!key) {
|
|
230
|
+
updateOfertaBanners();
|
|
231
|
+
updateClientSupportCard();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
151
234
|
try {
|
|
152
235
|
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
|
|
153
|
-
if (data.authenticated) {
|
|
236
|
+
if (data.authenticated && data.user) {
|
|
154
237
|
currentUser = { ...data.user, sessionKey: key };
|
|
155
238
|
onAuthenticated();
|
|
156
|
-
if (location.hash === '#addons') switchStage('addons', { scroll: false });
|
|
239
|
+
if (location.hash === '#addons' && isUserReadyForOrders()) switchStage('addons', { scroll: false });
|
|
157
240
|
}
|
|
158
241
|
}
|
|
159
242
|
catch { /* сессия устарела */ }
|
|
243
|
+
updateOfertaBanners();
|
|
244
|
+
updateClientSupportCard();
|
|
160
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
|
+
|
|
161
301
|
function onAuthenticated() {
|
|
162
302
|
if (!currentUser) return;
|
|
163
|
-
$('#tileAddons').style.display = '';
|
|
303
|
+
$('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
|
|
164
304
|
$('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
|
|
165
|
-
$('#inpLicId').value = currentUser.licenseId || '';
|
|
166
|
-
$('#
|
|
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();
|
|
167
325
|
moveIndicator();
|
|
168
326
|
recalcAddons();
|
|
169
327
|
}
|
|
@@ -215,54 +373,97 @@
|
|
|
215
373
|
recalcAddons();
|
|
216
374
|
}
|
|
217
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;
|
|
218
379
|
const extra = Math.max(0, Number($('#inpPakExtraSeats').value) || 0);
|
|
219
380
|
const tier = tierById(segValue('segPakTier'));
|
|
220
381
|
const disc = volumeDiscount(extra);
|
|
221
382
|
const gross = extra * tier.pricePerUserYearRub;
|
|
222
383
|
const net = gross - Math.round(gross * disc.discountPercent / 100);
|
|
223
384
|
$('#lblPakSeats').textContent = extra;
|
|
224
|
-
$('#lblPakTotal').textContent = rub(
|
|
385
|
+
$('#lblPakTotal').textContent = rub(basePrice + net);
|
|
225
386
|
const rows = [
|
|
226
|
-
['Комплекс ODANT-WORK', rub(
|
|
387
|
+
[isLicenseOnly ? 'Базовая лицензия (без железа)' : 'Комплекс ODANT-WORK (с машиной)', rub(basePrice)],
|
|
227
388
|
['Вечные рабочие места', String(D.product.baseSeats)],
|
|
228
389
|
['Поддержка в комплекте', '1 год · Enterprise (далее — по желанию)'],
|
|
229
390
|
];
|
|
230
391
|
if (extra > 0) rows.push([`Доп. места · ${esc(tier.name)}${disc.discountPercent ? ' · −' + disc.discountPercent + '%' : ''}`, `${extra} · ${rub(net)}`]);
|
|
231
392
|
$('#lblPakBreakdown').innerHTML = lines(rows);
|
|
232
393
|
}
|
|
233
|
-
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';
|
|
234
405
|
const st = $('#pakOrderStatus');
|
|
235
|
-
st.textContent = 'Формируем счёт ЮKassa…';
|
|
406
|
+
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
236
407
|
try {
|
|
237
408
|
const data = await api('/api/orders/checkout', {
|
|
238
|
-
|
|
409
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
410
|
+
includeBasePak: !isLicenseOnly,
|
|
411
|
+
includeBaseLicenseOnly: isLicenseOnly,
|
|
412
|
+
paymentMethod,
|
|
239
413
|
tier: segValue('segPakTier'),
|
|
240
414
|
addonSeats: Number($('#inpPakExtraSeats').value) || 0,
|
|
241
415
|
years: 1,
|
|
242
416
|
customer: { name: $('#inpPakOrg').value.trim(), inn: $('#inpPakInn').value.trim(), email: $('#inpPakEmail').value.trim() },
|
|
243
417
|
});
|
|
244
|
-
|
|
245
|
-
|
|
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();
|
|
246
425
|
}
|
|
247
426
|
catch (err) { st.textContent = err.message; }
|
|
248
427
|
}
|
|
249
|
-
async function submitAddonOrder() {
|
|
428
|
+
async function submitAddonOrder(paymentMethod = 'yookassa') {
|
|
429
|
+
if (!isUserReadyForOrders()) {
|
|
430
|
+
requireAuthAndOferta(() => submitAddonOrder(paymentMethod));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
250
433
|
const st = $('#orderStatus');
|
|
251
|
-
st.textContent = 'Формируем счёт ЮKassa…';
|
|
434
|
+
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
252
435
|
try {
|
|
253
436
|
const data = await api('/api/orders/checkout', {
|
|
437
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
254
438
|
includeBasePak: false,
|
|
439
|
+
paymentMethod,
|
|
255
440
|
tier: segValue('segTier'),
|
|
256
441
|
addonSeats: Number($('#inpSeats').value) || 1,
|
|
257
442
|
years: Number($('#inpYears').value) || 1,
|
|
258
443
|
licenseId: $('#inpLicId').value.trim(),
|
|
259
444
|
customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
|
|
260
445
|
});
|
|
261
|
-
|
|
262
|
-
|
|
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();
|
|
263
453
|
}
|
|
264
454
|
catch (err) { st.textContent = err.message; }
|
|
265
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
|
+
}
|
|
266
467
|
|
|
267
468
|
function recalcPartner() {
|
|
268
469
|
const m = Number($('#rngPartnerSales').value) || 0;
|
|
@@ -641,10 +842,19 @@
|
|
|
641
842
|
code = code || selectedCode;
|
|
642
843
|
if (!code) return;
|
|
643
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
|
+
}
|
|
644
853
|
const msg = $('#partnerActionMsg');
|
|
645
854
|
msg.textContent = 'Оформляем резерв и счёт на залог…';
|
|
646
855
|
try {
|
|
647
856
|
const data = await api('/api/partners/reserve', {
|
|
857
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
648
858
|
regionCode: code,
|
|
649
859
|
companyName: $('#inpPartnerCompany').value.trim(),
|
|
650
860
|
inn: $('#inpPartnerInn').value.trim(),
|
|
@@ -655,283 +865,47 @@
|
|
|
655
865
|
renderRegionsTable();
|
|
656
866
|
selectRegion(code);
|
|
657
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>`;
|
|
658
|
-
|
|
868
|
+
refreshCatalogRegions();
|
|
659
869
|
}
|
|
660
870
|
catch (err) { msg.textContent = err.message; }
|
|
661
871
|
}
|
|
662
872
|
window.reserveRegionFranchise = reserveRegionFranchise;
|
|
663
873
|
|
|
664
|
-
/* ================= Поддержка ================= */
|
|
874
|
+
/* ================= Поддержка и синхронизация квот ================= */
|
|
665
875
|
async function sendBot() {
|
|
666
876
|
const box = $('#botReplyBox');
|
|
667
877
|
try {
|
|
668
878
|
const data = await api('/api/bots/' + $('#botChannel').value + '/webhook', {
|
|
669
|
-
chat_id: $('#botChatId').value.trim(),
|
|
879
|
+
chat_id: $('#botChatId').value.trim(),
|
|
880
|
+
text: $('#botText').value,
|
|
881
|
+
user_name: currentUser?.companyName || 'Клиент ODANT-WORK',
|
|
670
882
|
});
|
|
671
883
|
box.textContent = data.reply || data.result?.reply || JSON.stringify(data, null, 2);
|
|
672
884
|
}
|
|
673
885
|
catch (err) { box.textContent = err.message; }
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
let lastAdminData = null;
|
|
685
|
-
|
|
686
|
-
async function refreshRegistry() {
|
|
687
|
-
let data;
|
|
688
|
-
try { data = await api('/api/admin/overview'); }
|
|
689
|
-
catch { return; }
|
|
690
|
-
lastAdminData = data;
|
|
691
|
-
if (Array.isArray(data.regions) && data.regions.length) {
|
|
692
|
-
data.regions.forEach(mergeRegion);
|
|
693
|
-
paintMap();
|
|
694
|
-
renderRegionsTable();
|
|
695
|
-
if (selectedCode) selectRegion(selectedCode);
|
|
696
|
-
}
|
|
697
|
-
const out = [];
|
|
698
|
-
out.push(`Лицензии (${data.licenses?.length || 0})`);
|
|
699
|
-
for (const l of data.licenses || [])
|
|
700
|
-
out.push(` ${l.licenseId} · ${l.customer?.name || '—'} · мест ${l.totalSeats} (${l.baseSeats ?? 10} вечных + ${l.addonSeats}) · ${l.support?.tierName || ''}`);
|
|
701
|
-
out.push('', `Резервы франшиз (${data.reservations?.length || 0})`);
|
|
702
|
-
for (const r of data.reservations || [])
|
|
703
|
-
out.push(` ${r.reservationId} · ${r.regionName} · ${r.companyName} · залог ${r.depositPaid ? 'оплачен' : 'ожидает оплаты'}`);
|
|
704
|
-
out.push('', `Обращения (${data.tickets?.length || 0})`);
|
|
705
|
-
for (const t of data.tickets || [])
|
|
706
|
-
out.push(` ${t.ticketId} [${String(t.channel).toUpperCase()}] ${t.tierName}: ${t.messages?.[0]?.text || ''}`);
|
|
707
|
-
if ($('#registryBox')) $('#registryBox').textContent = out.join('\n');
|
|
708
|
-
renderAdminPanel(data);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function fmtDate(ms) {
|
|
712
|
-
if (!ms) return '—';
|
|
713
|
-
try { return new Date(ms).toLocaleDateString('ru-RU'); } catch { return '—'; }
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function renderAdminPanel(data = {}) {
|
|
717
|
-
const licenses = data.licenses || [];
|
|
718
|
-
const orders = data.orders || [];
|
|
719
|
-
const partners = data.partners || [];
|
|
720
|
-
const reservations = data.reservations || [];
|
|
721
|
-
const tickets = data.tickets || [];
|
|
722
|
-
|
|
723
|
-
const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
|
|
724
|
-
const paidOrders = orders.filter(o => o.status === 'paid');
|
|
725
|
-
const pendingOrders = orders.filter(o => o.status === 'pending');
|
|
726
|
-
const paidRub = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
727
|
-
const pendingRub = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
728
|
-
const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
|
|
729
|
-
|
|
730
|
-
if ($('#admStatLic')) {
|
|
731
|
-
$('#admStatLic').textContent = String(licenses.length);
|
|
732
|
-
$('#admStatLicSub').textContent = `лицензий ПАК · ${totalSeats} активных мест`;
|
|
733
|
-
$('#admStatPaid').textContent = rub(paidRub);
|
|
734
|
-
$('#admStatPaidSub').textContent = `оплачено заказов: ${paidOrders.length}`;
|
|
735
|
-
$('#admStatPending').textContent = rub(pendingRub);
|
|
736
|
-
$('#admStatPendingSub').textContent = `ожидают оплаты: ${pendingOrders.length}`;
|
|
737
|
-
$('#admStatPartners').textContent = `${partners.length} / ${reservations.length}`;
|
|
738
|
-
$('#admStatTickets').textContent = String(openTickets.length);
|
|
739
|
-
$('#admStatTicketsSub').textContent = `открытых из ${tickets.length} обращений`;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if ($('#admLicensesBody')) {
|
|
743
|
-
$('#admLicensesBody').innerHTML = licenses.map(l => `<tr>
|
|
744
|
-
<td><b>${esc(l.licenseId)}</b><div class="small muted">Код бота: <code>${esc(l.bindingCode || l.licenseId)}</code></div></td>
|
|
745
|
-
<td><b>${esc(l.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(l.customer?.inn || '—')} · ${esc(l.customer?.email || '—')}</div></td>
|
|
746
|
-
<td><code>${esc(l.boundHwid || 'не привязан')}</code><div class="small muted">${esc((l.boundNodeFingerprint || '').slice(0, 18) || 'ожидает синхронизации')}</div></td>
|
|
747
|
-
<td class="num"><b>${l.totalSeats}</b> <span class="muted">(${l.baseSeats ?? 10} вечн. + ${l.addonSeats || 0} доп.)</span></td>
|
|
748
|
-
<td><span class="tag ok">${esc(l.support?.tierName || 'Enterprise')}</span><div class="small muted">до ${fmtDate(l.support?.expiresAt)}</div></td>
|
|
749
|
-
<td><button class="btn btn-ghost btn-sm" data-cert="${esc(l.licenseId)}">Сертификат JSON</button></td>
|
|
750
|
-
</tr>`).join('') || '<tr><td colspan="6" class="muted">Лицензий пока нет — нажмите «+ Демо-лицензия ПАК»</td></tr>';
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
if ($('#admOrdersBody')) {
|
|
754
|
-
$('#admOrdersBody').innerHTML = orders.map(o => {
|
|
755
|
-
const stTag = o.status === 'paid' ? 'ok' : (o.status === 'canceled' ? 'bad' : '');
|
|
756
|
-
const stLabel = o.status === 'paid' ? 'оплачен' : (o.status === 'canceled' ? 'отменён' : 'ожидает оплаты');
|
|
757
|
-
return `<tr>
|
|
758
|
-
<td><b>${esc(o.orderId)}</b><div class="small muted">${esc(o.paymentId || '—')}</div></td>
|
|
759
|
-
<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>
|
|
760
|
-
<td><b>${esc(o.customer?.name || '—')}</b><div class="small muted">ИНН ${esc(o.customer?.inn || '—')}</div></td>
|
|
761
|
-
<td class="num"><b>${rub(o.amountRub)}</b>${o.volumeDiscountPercent ? `<div class="small" style="color:var(--ok)">скидка −${o.volumeDiscountPercent}%</div>` : ''}</td>
|
|
762
|
-
<td><span class="tag ${stTag}">${stLabel}</span></td>
|
|
763
|
-
<td><div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
764
|
-
${o.status === 'pending' ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(o.orderId)}">Провести оплату</button>` : ''}
|
|
765
|
-
<a class="btn btn-ghost btn-sm" href="/pay/sandbox/${encodeURIComponent(o.orderId)}" target="_blank" rel="noopener">Песочница</a>
|
|
766
|
-
</div></td>
|
|
767
|
-
</tr>`;
|
|
768
|
-
}).join('') || '<tr><td colspan="6" class="muted">Заказов пока нет</td></tr>';
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if ($('#admPartnersBody')) {
|
|
772
|
-
const resRows = reservations.map(r => `<tr>
|
|
773
|
-
<td><b>${esc(r.reservationId)}</b><div class="small muted">Резерв франшизы</div></td>
|
|
774
|
-
<td><b>${esc(r.regionName)}</b> <span class="muted">(${esc(r.regionCode)})</span></td>
|
|
775
|
-
<td><b>${esc(r.companyName)}</b><div class="small muted">ИНН ${esc(r.inn || '—')} · ${esc(r.email || '—')}</div></td>
|
|
776
|
-
<td>Паушальный взнос <b>${rub(r.franchiseFeeRub)}</b><div class="small muted">Залог ${rub(r.nonRefundableDepositRub || r.depositAmountRub || D.franchise.reservationDepositRub)} · полная оплата за 1 год</div></td>
|
|
777
|
-
<td><span class="tag ${r.depositPaid ? 'ok' : ''}">${r.depositPaid ? 'залог оплачен' : 'ждёт оплаты залога'}</span></td>
|
|
778
|
-
<td><div style="display:flex;gap:6px;flex-wrap:wrap">
|
|
779
|
-
${!r.depositPaid && r.orderId ? `<button class="btn btn-ok btn-sm" data-pay-order="${esc(r.orderId)}">Оплатить залог</button>` : ''}
|
|
780
|
-
${!r.convertedToPartnerId ? `<button class="btn btn-primary btn-sm" data-activate-res="${esc(r.reservationId)}">Активировать партнёра</button>` : `<span class="small muted">Партнёр ${esc(r.convertedToPartnerId)}</span>`}
|
|
781
|
-
</div></td>
|
|
782
|
-
</tr>`);
|
|
783
|
-
const partRows = partners.map(p => `<tr>
|
|
784
|
-
<td><b>${esc(p.partnerId)}</b><div class="small muted">Действующий партнёр</div></td>
|
|
785
|
-
<td><b>${esc(p.regionName)}</b> <span class="muted">(${esc(p.regionCode)})</span></td>
|
|
786
|
-
<td><b>${esc(p.companyName)}</b><div class="small muted">ИНН ${esc(p.inn || '—')}</div></td>
|
|
787
|
-
<td>Скидка <b>${p.discountPercent}%</b> · ${esc(p.statusLevel)}<div class="small muted">Продажи: ${rub(p.annualSalesRub || 0)}</div></td>
|
|
788
|
-
<td><span class="tag ok">сертификатов: ${(p.certifiedSpecialists || []).length}</span></td>
|
|
789
|
-
<td><button class="btn btn-ghost btn-sm" data-certify-partner="${esc(p.partnerId)}">+ Аттестовать инженера</button></td>
|
|
790
|
-
</tr>`);
|
|
791
|
-
$('#admPartnersBody').innerHTML = [...resRows, ...partRows].join('') || '<tr><td colspan="6" class="muted">Резервов и партнёров пока нет — выберите субъект во вкладке «Франшиза»</td></tr>';
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if ($('#admTicketsBody')) {
|
|
795
|
-
$('#admTicketsBody').innerHTML = tickets.map(t => {
|
|
796
|
-
const msgs = (t.messages || []).map(m => `<div class="small"><b>${esc(m.role === 'engineer' ? (m.author || 'Инженер') : (t.userName || 'Клиент'))}:</b> ${esc(m.text)}</div>`).join('');
|
|
797
|
-
return `<tr>
|
|
798
|
-
<td><b>${esc(t.ticketId)}</b><div><span class="tag">${esc(String(t.channel || '').toUpperCase())}</span></div></td>
|
|
799
|
-
<td><b>${esc(t.userName || t.chatId)}</b><div class="small muted">Лицензия: ${esc(t.licenseId || 'не привязана')}</div></td>
|
|
800
|
-
<td><span class="tag ok">${esc(t.tierName || 'Start')}</span><div class="small muted">реакция ≤ ${t.slaReactionMinutes || 480} мин</div></td>
|
|
801
|
-
<td style="white-space:normal;max-width:360px">${msgs}</td>
|
|
802
|
-
<td>
|
|
803
|
-
<div style="display:flex;gap:6px">
|
|
804
|
-
<input type="text" id="reply-${esc(t.ticketId)}" placeholder="Ответ инженера…" style="min-width:190px;padding:7px 10px;font-size:12.5px">
|
|
805
|
-
<button class="btn btn-primary btn-sm" data-reply-ticket="${esc(t.ticketId)}">Ответить</button>
|
|
806
|
-
</div>
|
|
807
|
-
</td>
|
|
808
|
-
</tr>`;
|
|
809
|
-
}).join('') || '<tr><td colspan="5" class="muted">Обращений пока нет — отправьте сообщение через «Консоль бота» во вкладке «Поддержка»</td></tr>';
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
async function adminIssueLicense() {
|
|
814
|
-
const st = $('#admIssueStatus');
|
|
815
|
-
st.textContent = 'Выпускаем сертификат…';
|
|
816
|
-
try {
|
|
817
|
-
const lic = await api('/api/license/issue', {
|
|
818
|
-
hwid: $('#admLicHwid').value.trim(),
|
|
819
|
-
customer: {
|
|
820
|
-
name: $('#admLicOrg').value.trim(),
|
|
821
|
-
inn: $('#admLicInn').value.trim(),
|
|
822
|
-
email: $('#admLicEmail').value.trim(),
|
|
823
|
-
},
|
|
824
|
-
});
|
|
825
|
-
st.textContent = `Выпущена лицензия ${lic.licenseId} (${lic.totalSeats} мест, подпись Ed25519)`;
|
|
826
|
-
refreshRegistry();
|
|
827
|
-
}
|
|
828
|
-
catch (err) { st.textContent = err.message; }
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
async function adminConfirmOrder(orderId) {
|
|
832
|
-
const order = (lastAdminData?.orders || []).find(o => o.orderId === orderId);
|
|
833
|
-
if (!order) return;
|
|
834
|
-
await api('/api/payments/yookassa/webhook', {
|
|
835
|
-
type: 'notification',
|
|
836
|
-
event: 'payment.succeeded',
|
|
837
|
-
object: {
|
|
838
|
-
id: order.paymentId,
|
|
839
|
-
status: 'succeeded',
|
|
840
|
-
amount: { value: Number(order.amountRub || 0).toFixed(2), currency: 'RUB' },
|
|
841
|
-
metadata: { orderId: order.orderId, licenseId: order.licenseId || '', reservationId: order.reservationId || '' },
|
|
842
|
-
},
|
|
843
|
-
});
|
|
844
|
-
refreshRegistry();
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
async function adminActivateReservation(reservationId) {
|
|
848
|
-
const r = (lastAdminData?.reservations || []).find(x => x.reservationId === reservationId);
|
|
849
|
-
if (!r) return;
|
|
850
|
-
try {
|
|
851
|
-
await api('/api/partners/register', {
|
|
852
|
-
reservationId: r.reservationId,
|
|
853
|
-
regionCode: r.regionCode,
|
|
854
|
-
companyName: r.companyName,
|
|
855
|
-
inn: r.inn,
|
|
856
|
-
email: r.email,
|
|
857
|
-
franchiseFeePaid: true,
|
|
858
|
-
});
|
|
859
|
-
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 */ }
|
|
860
895
|
}
|
|
861
|
-
catch (err) { alert(err.message); }
|
|
862
896
|
}
|
|
863
897
|
|
|
864
|
-
async function
|
|
898
|
+
async function refreshCatalogRegions() {
|
|
865
899
|
try {
|
|
866
|
-
await api('/api/
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
});
|
|
873
|
-
refreshRegistry();
|
|
874
|
-
}
|
|
875
|
-
catch (err) { alert(err.message); }
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
async function adminReplyTicket(ticketId) {
|
|
879
|
-
const inp = document.getElementById('reply-' + ticketId);
|
|
880
|
-
const text = inp?.value.trim();
|
|
881
|
-
if (!text) return;
|
|
882
|
-
await api('/api/tickets/' + encodeURIComponent(ticketId) + '/reply', {
|
|
883
|
-
engineerName: 'Дежурный инженер ODANT',
|
|
884
|
-
text,
|
|
885
|
-
});
|
|
886
|
-
inp.value = '';
|
|
887
|
-
refreshRegistry();
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function openCertModal(licenseId) {
|
|
891
|
-
const lic = (lastAdminData?.licenses || []).find(l => l.licenseId === licenseId);
|
|
892
|
-
if (!lic) return;
|
|
893
|
-
$('#certTitle').textContent = `Сертификат ${lic.licenseId}`;
|
|
894
|
-
$('#certSub').textContent = `${lic.customer?.name || ''} · Ed25519 (WORK-LIC-v1)`;
|
|
895
|
-
$('#certJsonBox').textContent = JSON.stringify(lic.certificate || lic, null, 2);
|
|
896
|
-
$('#certModal').classList.add('open');
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
function switchAdminTab(tab = 'all') {
|
|
900
|
-
$$('[data-adm-tab]').forEach(b => b.classList.toggle('on', b.dataset.admTab === tab));
|
|
901
|
-
const ws = $('#adminWorkspace');
|
|
902
|
-
if (ws) ws.classList.toggle('only-agent', tab === 'agent');
|
|
903
|
-
$$('[data-adm-sec]').forEach(sec => {
|
|
904
|
-
sec.style.display = (tab === 'all' || tab === sec.dataset.admSec) ? '' : 'none';
|
|
905
|
-
});
|
|
906
|
-
if (tab === 'agent') setTimeout(() => $('#admAiInput')?.focus(), 50);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
async function sendAdminAi(text) {
|
|
910
|
-
const inp = $('#admAiInput');
|
|
911
|
-
text = (text ?? inp?.value ?? '').trim();
|
|
912
|
-
if (!text) return;
|
|
913
|
-
if (inp) inp.value = '';
|
|
914
|
-
const box = $('#admAiMessages');
|
|
915
|
-
if (!box) return;
|
|
916
|
-
box.insertAdjacentHTML('beforeend', `<div class="msg me">${esc(text)}</div><div class="msg bot" id="admAiTyping"><span class="typing"><i></i><i></i><i></i></span></div>`);
|
|
917
|
-
box.scrollTop = box.scrollHeight;
|
|
918
|
-
let data;
|
|
919
|
-
try { data = await api('/api/agent/chat', { message: text, sessionKey: currentUser?.sessionKey || '' }); }
|
|
920
|
-
catch (err) { data = { reply: err.message, actions: [] }; }
|
|
921
|
-
$('#admAiTyping')?.remove();
|
|
922
|
-
box.insertAdjacentHTML('beforeend', `<div class="msg bot">${md(data.reply || '')}</div>`);
|
|
923
|
-
box.scrollTop = box.scrollHeight;
|
|
924
|
-
for (const act of data.actions || []) {
|
|
925
|
-
if (act.type === 'refresh_admin') {
|
|
926
|
-
refreshRegistry();
|
|
927
|
-
}
|
|
928
|
-
else if (act.type === 'admin_tab' && act.tab) {
|
|
929
|
-
switchAdminTab(act.tab);
|
|
930
|
-
}
|
|
931
|
-
else if (act.type === 'select_region' && act.regionCode) {
|
|
932
|
-
selectRegion(act.regionCode, { zoom: true, scrollTable: false });
|
|
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);
|
|
933
906
|
}
|
|
934
907
|
}
|
|
908
|
+
catch { /* ignore */ }
|
|
935
909
|
}
|
|
936
910
|
|
|
937
911
|
/* ================= ИИ-агент лицензионного центра ================= */
|
|
@@ -962,21 +936,24 @@
|
|
|
962
936
|
box.insertAdjacentHTML('beforeend', `<div class="msg bot">${md(data.reply || '')}</div>`);
|
|
963
937
|
box.scrollTop = box.scrollHeight;
|
|
964
938
|
for (const act of data.actions || []) {
|
|
965
|
-
if (act.type === '
|
|
966
|
-
refreshRegistry();
|
|
967
|
-
}
|
|
968
|
-
else if (act.type === 'select_region') {
|
|
939
|
+
if (act.type === 'select_region') {
|
|
969
940
|
switchStage('franchise');
|
|
970
941
|
setTimeout(() => selectRegion(act.regionCode, { zoom: true, scrollTable: true }), 80);
|
|
971
942
|
}
|
|
972
|
-
else if (act.type === 'prefill_calculator' &&
|
|
943
|
+
else if (act.type === 'prefill_calculator' && isUserReadyForOrders()) {
|
|
973
944
|
$$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === act.tier));
|
|
974
945
|
if (act.years) $('#inpYears').value = act.years;
|
|
975
946
|
syncSeats(act.seats);
|
|
976
947
|
switchStage('addons');
|
|
977
948
|
}
|
|
978
|
-
else if (act.type === '
|
|
979
|
-
|
|
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);
|
|
980
957
|
}
|
|
981
958
|
}
|
|
982
959
|
}
|
|
@@ -1146,16 +1123,9 @@
|
|
|
1146
1123
|
/* ================= Привязка событий ================= */
|
|
1147
1124
|
function bind() {
|
|
1148
1125
|
document.addEventListener('click', (e) => {
|
|
1149
|
-
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-
|
|
1126
|
+
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-bot],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
|
|
1150
1127
|
if (!el) return;
|
|
1151
1128
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1152
|
-
if (el.matches('[data-adm-tab]')) { switchAdminTab(el.dataset.admTab); return; }
|
|
1153
|
-
if (el.matches('[data-adm-ask]')) { sendAdminAi(el.dataset.admAsk); return; }
|
|
1154
|
-
if (el.matches('[data-cert]')) { openCertModal(el.dataset.cert); return; }
|
|
1155
|
-
if (el.matches('[data-pay-order]')) { adminConfirmOrder(el.dataset.payOrder); return; }
|
|
1156
|
-
if (el.matches('[data-activate-res]')) { adminActivateReservation(el.dataset.activateRes); return; }
|
|
1157
|
-
if (el.matches('[data-certify-partner]')) { adminCertifyPartner(el.dataset.certifyPartner); return; }
|
|
1158
|
-
if (el.matches('[data-reply-ticket]')) { adminReplyTicket(el.dataset.replyTicket); return; }
|
|
1159
1129
|
if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
|
|
1160
1130
|
if (el.matches('th[data-sort]')) {
|
|
1161
1131
|
SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
|
|
@@ -1168,23 +1138,35 @@
|
|
|
1168
1138
|
if (el.matches('[data-ask]')) { sendAi(el.dataset.ask); return; }
|
|
1169
1139
|
if (el.dataset.stage) { e.preventDefault(); switchStage(el.dataset.stage); return; }
|
|
1170
1140
|
switch (el.dataset.action) {
|
|
1171
|
-
case 'auth':
|
|
1141
|
+
case 'auth': openAuth(); break;
|
|
1172
1142
|
case 'auth-close': closeAuth(); break;
|
|
1173
1143
|
case 'login': performLogin(); break;
|
|
1174
|
-
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;
|
|
1175
1157
|
case 'pick-tier':
|
|
1176
1158
|
$$('#segTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
|
|
1177
1159
|
recalcAddons();
|
|
1178
|
-
if (
|
|
1160
|
+
if (isUserReadyForOrders()) switchStage('addons');
|
|
1161
|
+
else requireAuthAndOferta(() => switchStage('addons'));
|
|
1179
1162
|
break;
|
|
1180
|
-
case 'order-pak': submitBasePakOrder(); break;
|
|
1181
|
-
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;
|
|
1182
1168
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
1183
1169
|
case 'bot-send': sendBot(); break;
|
|
1184
|
-
case 'registry': refreshRegistry(); break;
|
|
1185
|
-
case 'demo-pak': createDemoPak(); break;
|
|
1186
|
-
case 'adm-issue-lic': adminIssueLicense(); break;
|
|
1187
|
-
case 'adm-ai-send': sendAdminAi(); break;
|
|
1188
1170
|
case 'cert-close': $('#certModal')?.classList.remove('open'); break;
|
|
1189
1171
|
case 'cert-copy':
|
|
1190
1172
|
navigator.clipboard?.writeText($('#certJsonBox')?.textContent || '');
|
|
@@ -1193,23 +1175,24 @@
|
|
|
1193
1175
|
case 'ai-send': sendAi(); break;
|
|
1194
1176
|
}
|
|
1195
1177
|
});
|
|
1196
|
-
$('#authModal')
|
|
1178
|
+
$('#authModal')?.addEventListener('click', (e) => { if (e.target.id === 'authModal') closeAuth(); });
|
|
1179
|
+
$('#ofertaModal')?.addEventListener('click', (e) => { if (e.target.id === 'ofertaModal') closeOfertaModal(); });
|
|
1197
1180
|
$('#certModal')?.addEventListener('click', (e) => { if (e.target.id === 'certModal') $('#certModal').classList.remove('open'); });
|
|
1198
1181
|
document.addEventListener('keydown', (e) => {
|
|
1199
|
-
if (e.key === 'Escape') { closeAuth(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
|
|
1182
|
+
if (e.key === 'Escape') { closeAuth(); closeOfertaModal(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
|
|
1200
1183
|
});
|
|
1201
|
-
$('#authModal')
|
|
1202
|
-
$('#aiInput')
|
|
1203
|
-
$('#
|
|
1204
|
-
$('#
|
|
1205
|
-
$('#
|
|
1206
|
-
$('#
|
|
1207
|
-
$('#
|
|
1208
|
-
$('#
|
|
1209
|
-
$('#
|
|
1210
|
-
$('#inpRegionFilter').addEventListener('input', renderRegionsTable);
|
|
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);
|
|
1211
1193
|
bindSeg('segTier', recalcAddons);
|
|
1212
1194
|
bindSeg('segPakTier', recalcBaseOrder);
|
|
1195
|
+
bindSeg('segPakVariant', recalcBaseOrder);
|
|
1213
1196
|
window.addEventListener('resize', moveIndicator);
|
|
1214
1197
|
window.addEventListener('hashchange', () => { const id = location.hash.slice(1); if (id && id !== currentStage) switchStage(id, { scroll: false }); });
|
|
1215
1198
|
}
|
|
@@ -1223,12 +1206,12 @@
|
|
|
1223
1206
|
recalcPartner();
|
|
1224
1207
|
renderRegionsTable();
|
|
1225
1208
|
initBackdrop();
|
|
1226
|
-
const initial = D.initialStage ||
|
|
1227
|
-
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 });
|
|
1228
1211
|
if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
|
|
1229
1212
|
initMap().then(() => selectRegion(selectedCode || 'RU-SVE'));
|
|
1230
1213
|
restoreSession();
|
|
1231
|
-
|
|
1214
|
+
refreshCatalogRegions();
|
|
1232
1215
|
}
|
|
1233
1216
|
|
|
1234
1217
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
|