work-license-server 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/landing.js +309 -228
- package/package.json +1 -1
- package/src/landing.js +123 -179
- package/src/server.js +86 -1
- package/src/store.js +110 -15
package/assets/landing.js
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
if (scroll) window.scrollTo({ top: 0, behavior: reduceMotion ? 'auto' : 'smooth' });
|
|
81
81
|
if (window.setBackdropStage) window.setBackdropStage(id);
|
|
82
82
|
observeReveals();
|
|
83
|
-
if (id === '
|
|
83
|
+
if (id === 'product') playConsultantMock();
|
|
84
84
|
if (id === 'franchise') requestAnimationFrame(() => MAP.ready && applyView());
|
|
85
85
|
}
|
|
86
86
|
window.switchStage = switchStage;
|
|
@@ -129,15 +129,26 @@
|
|
|
129
129
|
card.style.setProperty('--my', (e.clientY - r.top) + 'px');
|
|
130
130
|
}, { passive: true });
|
|
131
131
|
|
|
132
|
-
/* =================
|
|
133
|
-
function openAuth(hintMsg = '') {
|
|
132
|
+
/* ================= Вход / Регистрация (Email, Мессенджеры, OAuth) ================= */
|
|
133
|
+
function openAuth(hintMsg = '', intentTitle = '') {
|
|
134
134
|
if ($('#authErrorMsg')) $('#authErrorMsg').textContent = hintMsg || '';
|
|
135
|
+
const intentBox = $('#authIntentNotice');
|
|
136
|
+
if (intentBox) {
|
|
137
|
+
if (intentTitle) {
|
|
138
|
+
intentBox.style.display = '';
|
|
139
|
+
intentBox.innerHTML = `Выбранная позиция: <b>${esc(intentTitle)}</b>.<br>После входа она автоматически появится как заказ в вашем Личном кабинете.`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
intentBox.style.display = 'none';
|
|
143
|
+
intentBox.textContent = '';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
135
146
|
if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = currentUser ? '' : 'none';
|
|
136
|
-
$('#authModal')
|
|
147
|
+
$('#authModal')?.classList.add('open');
|
|
137
148
|
setTimeout(() => $('#authEmail')?.focus(), 50);
|
|
138
149
|
}
|
|
139
150
|
function closeAuth() {
|
|
140
|
-
$('#authModal')
|
|
151
|
+
$('#authModal')?.classList.remove('open');
|
|
141
152
|
if ($('#authErrorMsg')) $('#authErrorMsg').textContent = '';
|
|
142
153
|
}
|
|
143
154
|
function openOfertaModal() {
|
|
@@ -146,49 +157,38 @@
|
|
|
146
157
|
function closeOfertaModal() {
|
|
147
158
|
$('#ofertaModal')?.classList.remove('open');
|
|
148
159
|
}
|
|
149
|
-
function requireAuthAndOferta(actionFn,
|
|
160
|
+
function requireAuthAndOferta(actionFn, opts = {}) {
|
|
150
161
|
if (isUserReadyForOrders()) {
|
|
151
162
|
if (typeof actionFn === 'function') actionFn();
|
|
152
163
|
return true;
|
|
153
164
|
}
|
|
154
165
|
pendingAuthAction = typeof actionFn === 'function' ? actionFn : null;
|
|
155
|
-
|
|
156
|
-
if (prefill.inn && $('#authInn')) $('#authInn').value = prefill.inn;
|
|
157
|
-
if (prefill.email && $('#authEmail')) $('#authEmail').value = prefill.email;
|
|
158
|
-
openAuth('Для оформления заказа, оплаты или резервирования франшизы зарегистрируйтесь и подпишите договор-оферту (ПЭП).');
|
|
166
|
+
openAuth('', opts.intentTitle || '');
|
|
159
167
|
return false;
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
async function performLogin() {
|
|
170
|
+
async function performLogin(provider = 'email') {
|
|
163
171
|
const errBox = $('#authErrorMsg');
|
|
164
172
|
if (errBox) errBox.textContent = '';
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const inn = $('#authInn')?.value.trim() || '';
|
|
168
|
-
const signer = $('#authSigner')?.value.trim() || '';
|
|
169
|
-
const licenseId = $('#authLicId')?.value.trim() || '';
|
|
170
|
-
const ofertaChecked = Boolean($('#authOfertaCheck')?.checked);
|
|
173
|
+
let email = $('#authEmail')?.value.trim() || '';
|
|
174
|
+
const ofertaChecked = $('#authOfertaCheck') ? Boolean($('#authOfertaCheck').checked) : true;
|
|
171
175
|
|
|
172
|
-
if (!
|
|
173
|
-
if (errBox) errBox.textContent = '
|
|
176
|
+
if (!ofertaChecked) {
|
|
177
|
+
if (errBox) errBox.textContent = 'Подтвердите согласие с условиями Публичного договора-оферты.';
|
|
174
178
|
return;
|
|
175
179
|
}
|
|
176
|
-
if (!
|
|
177
|
-
if (errBox) errBox.textContent = 'Укажите
|
|
180
|
+
if (provider === 'email' && (!email || !email.includes('@'))) {
|
|
181
|
+
if (errBox) errBox.textContent = 'Укажите корректный email.';
|
|
178
182
|
return;
|
|
179
183
|
}
|
|
180
|
-
if (!
|
|
181
|
-
|
|
182
|
-
return;
|
|
184
|
+
if (provider !== 'email' && (!email || !email.includes('@'))) {
|
|
185
|
+
email = `user.${provider}@${provider}.id.odant.org`;
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
try {
|
|
186
189
|
const data = await api('/api/auth/register', {
|
|
187
190
|
email,
|
|
188
|
-
|
|
189
|
-
inn,
|
|
190
|
-
name: signer || companyName,
|
|
191
|
-
licenseId,
|
|
191
|
+
provider,
|
|
192
192
|
ofertaAccepted: true,
|
|
193
193
|
});
|
|
194
194
|
currentUser = data;
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
const nextAction = pendingAuthAction;
|
|
199
199
|
pendingAuthAction = null;
|
|
200
200
|
if (typeof nextAction === 'function') {
|
|
201
|
-
nextAction();
|
|
201
|
+
await nextAction();
|
|
202
202
|
}
|
|
203
203
|
else {
|
|
204
204
|
switchStage('addons');
|
|
@@ -213,11 +213,9 @@
|
|
|
213
213
|
currentUser = null;
|
|
214
214
|
pendingAuthAction = null;
|
|
215
215
|
try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
|
|
216
|
-
$('#tileAddons').style.display = 'none';
|
|
217
|
-
$('#btnAuthText').textContent = 'Войти / Регистрация';
|
|
216
|
+
if ($('#tileAddons')) $('#tileAddons').style.display = 'none';
|
|
217
|
+
if ($('#btnAuthText')) $('#btnAuthText').textContent = 'Войти / Регистрация';
|
|
218
218
|
if ($('#btnLogoutModal')) $('#btnLogoutModal').style.display = 'none';
|
|
219
|
-
updateOfertaBanners();
|
|
220
|
-
updateClientSupportCard();
|
|
221
219
|
closeAuth();
|
|
222
220
|
if (currentStage === 'addons') switchStage('product');
|
|
223
221
|
moveIndicator();
|
|
@@ -226,11 +224,7 @@
|
|
|
226
224
|
async function restoreSession() {
|
|
227
225
|
let key = '';
|
|
228
226
|
try { key = localStorage.getItem('odant_work_session') || ''; } catch { /* приватный режим */ }
|
|
229
|
-
if (!key)
|
|
230
|
-
updateOfertaBanners();
|
|
231
|
-
updateClientSupportCard();
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
227
|
+
if (!key) return;
|
|
234
228
|
try {
|
|
235
229
|
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(key));
|
|
236
230
|
if (data.authenticated && data.user) {
|
|
@@ -240,93 +234,129 @@
|
|
|
240
234
|
}
|
|
241
235
|
}
|
|
242
236
|
catch { /* сессия устарела */ }
|
|
243
|
-
updateOfertaBanners();
|
|
244
|
-
updateClientSupportCard();
|
|
245
237
|
}
|
|
246
238
|
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
239
|
+
function onAuthenticated() {
|
|
240
|
+
if (!currentUser) return;
|
|
241
|
+
if ($('#tileAddons')) $('#tileAddons').style.display = isUserReadyForOrders() ? '' : 'none';
|
|
242
|
+
if ($('#btnAuthText')) $('#btnAuthText').textContent = currentUser.companyName || currentUser.email;
|
|
243
|
+
if ($('#inpCabinetHubToken')) $('#inpCabinetHubToken').value = currentUser.hubToken || '';
|
|
244
|
+
if ($('#authEmail')) $('#authEmail').value = currentUser.email || '';
|
|
245
|
+
if ($('#authOfertaCheck')) $('#authOfertaCheck').checked = Boolean(currentUser.ofertaAccepted);
|
|
246
|
+
if ($('#userBadgeBox')) {
|
|
247
|
+
const orgInfo = currentUser.companyName ? `${esc(currentUser.companyName)}${currentUser.inn ? ' (ИНН ' + esc(currentUser.inn) + ')' : ''} · ` : '';
|
|
248
|
+
$('#userBadgeBox').innerHTML = `${orgInfo}Аккаунт: <b>${esc(currentUser.email)}</b> · <a href="#" data-action="logout" style="color:var(--muted)">Выйти</a>`;
|
|
249
|
+
}
|
|
250
|
+
renderCabinetOrders();
|
|
251
|
+
renderCabinetLicenses();
|
|
252
|
+
moveIndicator();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderCabinetOrders() {
|
|
256
|
+
const box = $('#cabinetOrdersBox');
|
|
257
|
+
if (!box) return;
|
|
258
|
+
const orders = currentUser?.orders || [];
|
|
259
|
+
if (!orders.length) {
|
|
260
|
+
box.innerHTML = `<div style="padding:18px;border-radius:12px;border:1px dashed var(--border);color:var(--muted);text-align:center;">
|
|
261
|
+
В вашем Личном кабинете пока нет заказов. Перейдите в раздел <a href="#clients" data-stage="clients"><b>«Клиентам»</b></a> (чтобы купить лицензию или ПАК) или <a href="#franchise" data-stage="franchise"><b>«Франшиза»</b></a> (чтобы зарезервировать регион).
|
|
262
|
+
</div>`;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
box.innerHTML = orders.map(o => {
|
|
266
|
+
const isPaid = o.status === 'paid';
|
|
267
|
+
const title = (o.items && o.items.length)
|
|
268
|
+
? o.items.map(it => esc(it.title)).join(' + ')
|
|
269
|
+
: esc(o.tierName || o.orderId);
|
|
270
|
+
const dateStr = o.createdAt ? new Date(o.createdAt).toLocaleString('ru-RU') : '';
|
|
271
|
+
const statusBadge = isPaid
|
|
272
|
+
? `<span class="tag ok">✓ Оплачен</span>`
|
|
273
|
+
: `<span class="tag">Ожидает оплаты</span>`;
|
|
274
|
+
const actionsHtml = isPaid
|
|
275
|
+
? `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
|
276
|
+
${o.invoiceUrl ? `<a class="btn btn-ghost btn-sm" href="${esc(o.invoiceUrl)}" target="_blank" rel="noopener">Открыть счёт-договор (${esc(o.invoiceNumber || o.orderId)}) ↗</a>` : ''}
|
|
277
|
+
</div>`
|
|
278
|
+
: `<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:12px;align-items:center">
|
|
279
|
+
<button class="btn btn-primary btn-sm" data-pay-order="${esc(o.orderId)}" data-pay-method="yookassa">Оплатить через ЮKassa (${rub(o.amountRub)})</button>
|
|
280
|
+
<button class="btn btn-ghost btn-sm" data-invoice-toggle="${esc(o.orderId)}">Счёт-договор (для юр. лиц)</button>
|
|
281
|
+
<button class="btn btn-ghost btn-sm" data-cancel-order="${esc(o.orderId)}" style="color:var(--danger)">Удалить заказ</button>
|
|
282
|
+
</div>
|
|
283
|
+
<div id="invBox-${esc(o.orderId)}" style="display:none;margin-top:12px;padding:12px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.02)">
|
|
284
|
+
<div class="small" style="margin-bottom:8px;font-weight:600">Реквизиты организации для выставления счёта-договора:</div>
|
|
285
|
+
<div class="row2">
|
|
286
|
+
<div class="field"><label>Наименование организации *</label><input type="text" id="invCompany-${esc(o.orderId)}" value="${esc(o.customer?.name || currentUser?.companyName || '')}" placeholder="ООО «Маслоторг»"></div>
|
|
287
|
+
<div class="field"><label>ИНН *</label><input type="text" id="invInn-${esc(o.orderId)}" value="${esc(o.customer?.inn || currentUser?.inn || '')}" placeholder="7701234567" inputmode="numeric"></div>
|
|
258
288
|
</div>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
box.innerHTML = `
|
|
289
|
+
<button class="btn btn-primary btn-sm" style="margin-top:8px" data-pay-order="${esc(o.orderId)}" data-pay-method="invoice">Сформировать и открыть счёт-договор →</button>
|
|
290
|
+
</div>`;
|
|
291
|
+
return `<div style="padding:14px 16px;border-radius:12px;border:1px solid var(--border);margin-bottom:10px;">
|
|
292
|
+
<div style="display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center">
|
|
266
293
|
<div>
|
|
267
|
-
<
|
|
268
|
-
<div style="
|
|
269
|
-
Любые действия по заказу комплекса, докупке лицензий, оплате через ЮKassa и резервированию региональной франшизы выполняются после регистрации и подписания Публичного договора-оферты (ПЭП).
|
|
270
|
-
</div>
|
|
294
|
+
<div style="font-size:12px;color:var(--muted)">Заказ <code>${esc(o.orderId)}</code> · ${esc(dateStr)}</div>
|
|
295
|
+
<div style="font-weight:700;font-size:15px;margin-top:2px">${title}</div>
|
|
271
296
|
</div>
|
|
272
|
-
<div style="
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
</div
|
|
276
|
-
|
|
277
|
-
|
|
297
|
+
<div style="text-align:right">
|
|
298
|
+
<div style="font-size:18px;font-weight:800">${rub(o.amountRub)}</div>
|
|
299
|
+
${statusBadge}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
${actionsHtml}
|
|
303
|
+
</div>`;
|
|
304
|
+
}).join('');
|
|
278
305
|
}
|
|
279
306
|
|
|
280
|
-
function
|
|
281
|
-
const box = $('#
|
|
307
|
+
function renderCabinetLicenses() {
|
|
308
|
+
const box = $('#cabinetLicensesBox');
|
|
282
309
|
if (!box) return;
|
|
283
|
-
|
|
284
|
-
|
|
310
|
+
const lic = currentUser?.license;
|
|
311
|
+
const files = lic?.licenseFiles || [];
|
|
312
|
+
const reservations = (currentUser?.reservations || []).filter(r => r.depositPaid);
|
|
313
|
+
if (!files.length && !reservations.length) {
|
|
314
|
+
box.innerHTML = `<div style="padding:12px 14px;border-radius:10px;border:1px dashed var(--border);color:var(--muted);">
|
|
315
|
+
Активных оплаченных лицензий пока нет. После оплаты заказа выше подписанные файлы лицензий (<code>.lic.json</code>) сразу появятся здесь и автоматически загрузятся в ваш локальный WORK по токену владельца.
|
|
316
|
+
</div>`;
|
|
285
317
|
return;
|
|
286
318
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
319
|
+
const rows = files.map(f => {
|
|
320
|
+
const lf = f.licenseFile || f;
|
|
321
|
+
const isBase = lf.kind === 'base' || lf.perpetual;
|
|
322
|
+
const expText = isBase ? 'Бессрочно (постоянная)' : (lf.expiresAt ? 'до ' + new Date(lf.expiresAt).toLocaleDateString('ru-RU') + ` (${lf.years || 1} г.)` : '—');
|
|
323
|
+
const title = isBase ? 'Первичная постоянная лицензия (10 вечных мест + ИИ-агенты)' : `Временная лицензия (+${lf.seats} конк. мест · ${String(lf.tier || '').toUpperCase()})`;
|
|
324
|
+
return `<div style="display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);align-items:center;">
|
|
325
|
+
<div><b>${esc(title)}</b><br><code style="font-size:11.5px;color:var(--muted)">${esc(f.fileName || lf.fileId)}</code></div>
|
|
326
|
+
<div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">${esc(expText)}</span></div>
|
|
327
|
+
</div>`;
|
|
328
|
+
}).join('');
|
|
329
|
+
const resRows = reservations.map(r => `<div style="display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);align-items:center;">
|
|
330
|
+
<div><b>Региональная франшиза: ${esc(r.regionName)}</b><br><code style="font-size:11.5px;color:var(--muted)">${esc(r.reservationId)}</code></div>
|
|
331
|
+
<div style="text-align:right"><span class="pill" style="padding:3px 10px;font-size:11.5px">Залог оплачен</span></div>
|
|
332
|
+
</div>`).join('');
|
|
333
|
+
box.innerHTML = (files.length ? `<div>Всего доступно одновременных сессий: <b style="color:var(--text)">${lic.totalSeats}</b> (${lic.baseSeats} вечных + ${lic.addonSeats} временных)</div>` : '') + rows + resRows;
|
|
299
334
|
}
|
|
300
335
|
|
|
301
|
-
function
|
|
302
|
-
if (!currentUser) return;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
336
|
+
async function refreshCabinetUser() {
|
|
337
|
+
if (!currentUser?.sessionKey) return;
|
|
338
|
+
try {
|
|
339
|
+
const data = await api('/api/auth/me?sessionKey=' + encodeURIComponent(currentUser.sessionKey));
|
|
340
|
+
if (data.authenticated && data.user) {
|
|
341
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
342
|
+
onAuthenticated();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch { /* ignore */ }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function copyCabinetToken() {
|
|
349
|
+
const val = $('#inpCabinetHubToken')?.value || '';
|
|
350
|
+
if (!val) return;
|
|
351
|
+
navigator.clipboard?.writeText(val);
|
|
352
|
+
const st = $('#tokenCopyStatus');
|
|
353
|
+
if (st) {
|
|
354
|
+
st.textContent = 'Токен скопирован в буфер обмена! Вставьте его в локальном WORK → Лицензия.';
|
|
355
|
+
setTimeout(() => { if (st.textContent.includes('скопирован')) st.textContent = ''; }, 5000);
|
|
321
356
|
}
|
|
322
|
-
$('#userBadgeBox').innerHTML = `${esc(currentUser.companyName)} (ИНН ${esc(currentUser.inn || '—')}) · лицензия <b>${esc(currentUser.licenseId)}</b> · ПЭП <code>${esc(currentUser.ofertaSignatureId || '—')}</code>`;
|
|
323
|
-
updateOfertaBanners();
|
|
324
|
-
updateClientSupportCard();
|
|
325
|
-
moveIndicator();
|
|
326
|
-
recalcAddons();
|
|
327
357
|
}
|
|
328
358
|
|
|
329
|
-
/* ================= Калькуляторы ================= */
|
|
359
|
+
/* ================= Калькуляторы раздела «Клиентам» ================= */
|
|
330
360
|
function volumeDiscount(seats) {
|
|
331
361
|
const s = Math.max(0, Math.floor(Number(seats) || 0));
|
|
332
362
|
return (D.discounts || []).find(d => s >= d.minSeats) || { discountPercent: 0, minSeats: 1 };
|
|
@@ -345,114 +375,147 @@
|
|
|
345
375
|
const tierById = (id) => (D.tiers || []).find(t => t.id === id) || D.tiers[1];
|
|
346
376
|
const lines = (rows) => rows.map(([k, v]) => `<div><span>${k}</span><b>${v}</b></div>`).join('');
|
|
347
377
|
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
$('#lblTotal').textContent = rub(gross - saved);
|
|
356
|
-
$('#lblDiscountBadge').textContent = disc.discountPercent
|
|
357
|
-
? `Скидка за количество −${disc.discountPercent}% · экономия ${rub(saved)}`
|
|
358
|
-
: 'Скидка от количества начинается с 5 мест';
|
|
359
|
-
$('#lblBreakdown').innerHTML = lines([
|
|
360
|
-
['Уровень поддержки', `${esc(tier.name)} · ${esc(tier.reactionLabel)}`],
|
|
361
|
-
['Цена места в год', rub(tier.pricePerUserYearRub)],
|
|
362
|
-
['Мест × лет', `${seats} × ${years}`],
|
|
363
|
-
['Без скидки', rub(gross)],
|
|
364
|
-
['Скидка', `−${disc.discountPercent}%`],
|
|
365
|
-
['Всего мест после докупки', `${(D.product?.baseSeats || 10) + seats}`],
|
|
366
|
-
]);
|
|
367
|
-
markLadder('ladderAddons', seats);
|
|
368
|
-
}
|
|
369
|
-
function syncSeats(v) {
|
|
370
|
-
const n = Math.max(1, Math.min(2000, Number(v) || 1));
|
|
371
|
-
$('#inpSeats').value = n;
|
|
372
|
-
$('#rngAddonSeats').value = Math.min(150, n);
|
|
373
|
-
recalcAddons();
|
|
378
|
+
function syncClientSeats(v) {
|
|
379
|
+
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
380
|
+
const minSeats = variant === 'addons_only' ? 1 : 0;
|
|
381
|
+
const n = Math.max(minSeats, Math.min(2000, Number(v) || 0));
|
|
382
|
+
if ($('#inpPakExtraSeats')) $('#inpPakExtraSeats').value = Math.min(150, n);
|
|
383
|
+
if ($('#inpPakExtraSeatsNum')) $('#inpPakExtraSeatsNum').value = n;
|
|
384
|
+
recalcBaseOrder();
|
|
374
385
|
}
|
|
386
|
+
|
|
375
387
|
function recalcBaseOrder() {
|
|
388
|
+
if (!$('#lblPakTotal')) return;
|
|
376
389
|
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
const
|
|
390
|
+
const includeBasePak = variant === 'pak';
|
|
391
|
+
const includeBaseLicenseOnly = variant === 'license_only';
|
|
392
|
+
const basePrice = includeBasePak
|
|
393
|
+
? (D.product?.basePriceRub || 5_000_000)
|
|
394
|
+
: (includeBaseLicenseOnly ? (D.product?.baseLicensePriceRub || 4_000_000) : 0);
|
|
395
|
+
|
|
396
|
+
const minSeats = variant === 'addons_only' ? 1 : 0;
|
|
397
|
+
const extra = Math.max(minSeats, Number($('#inpPakExtraSeatsNum')?.value ?? $('#inpPakExtraSeats')?.value) || 0);
|
|
398
|
+
if ($('#inpPakExtraSeatsNum') && Number($('#inpPakExtraSeatsNum').value) !== extra) {
|
|
399
|
+
$('#inpPakExtraSeatsNum').value = extra;
|
|
400
|
+
}
|
|
401
|
+
const years = Math.min(5, Math.max(1, Number($('#inpPakYears')?.value) || 1));
|
|
380
402
|
const tier = tierById(segValue('segPakTier'));
|
|
381
403
|
const disc = volumeDiscount(extra);
|
|
382
|
-
const gross = extra * tier.pricePerUserYearRub;
|
|
383
|
-
const
|
|
384
|
-
|
|
404
|
+
const gross = extra * tier.pricePerUserYearRub * years;
|
|
405
|
+
const saved = Math.round(gross * disc.discountPercent / 100);
|
|
406
|
+
const net = gross - saved;
|
|
407
|
+
|
|
385
408
|
$('#lblPakTotal').textContent = rub(basePrice + net);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
409
|
+
if ($('#lblPakDiscount')) {
|
|
410
|
+
$('#lblPakDiscount').textContent = extra > 0 && disc.discountPercent
|
|
411
|
+
? `Скидка за количество −${disc.discountPercent}% · экономия ${rub(saved)}`
|
|
412
|
+
: (includeBasePak ? 'Включает сервер, 10 вечных мест и 1 год поддержки Enterprise' : (includeBaseLicenseOnly ? 'Включает 10 вечных мест и активацию ИИ-агентов' : 'Скидка от количества начинается с 5 мест'));
|
|
413
|
+
}
|
|
414
|
+
const rows = [];
|
|
415
|
+
if (includeBasePak) rows.push(['Комплекс ODANT-WORK (с машиной)', rub(basePrice)]);
|
|
416
|
+
if (includeBaseLicenseOnly) rows.push(['Базовая лицензия (без железа)', rub(basePrice)]);
|
|
417
|
+
if (includeBasePak || includeBaseLicenseOnly) rows.push(['Вечные рабочие места', String(D.product.baseSeats)]);
|
|
418
|
+
if (extra > 0) {
|
|
419
|
+
rows.push([`Доп. места · ${esc(tier.name)} (${years} г.)`, `${extra} мест · ${rub(net)}`]);
|
|
420
|
+
}
|
|
392
421
|
$('#lblPakBreakdown').innerHTML = lines(rows);
|
|
422
|
+
markLadder('ladderPricing', extra);
|
|
393
423
|
}
|
|
394
|
-
|
|
424
|
+
|
|
425
|
+
async function buyClientConfig() {
|
|
426
|
+
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
427
|
+
const includeBasePak = variant === 'pak';
|
|
428
|
+
const includeBaseLicenseOnly = variant === 'license_only';
|
|
429
|
+
const minSeats = variant === 'addons_only' ? 1 : 0;
|
|
430
|
+
const addonSeats = Math.max(minSeats, Number($('#inpPakExtraSeatsNum')?.value ?? $('#inpPakExtraSeats')?.value) || 0);
|
|
431
|
+
const years = Math.min(5, Math.max(1, Number($('#inpPakYears')?.value) || 1));
|
|
432
|
+
const tier = segValue('segPakTier');
|
|
433
|
+
|
|
434
|
+
const intentTitle = includeBasePak
|
|
435
|
+
? `Комплекс ODANT-WORK с машиной${addonSeats > 0 ? ` + ${addonSeats} доп. мест` : ''}`
|
|
436
|
+
: (includeBaseLicenseOnly
|
|
437
|
+
? `Базовая лицензия без железа (10 вечных мест)${addonSeats > 0 ? ` + ${addonSeats} доп. мест` : ''}`
|
|
438
|
+
: `Дополнительные временные лицензии (${addonSeats} мест на ${years} г.)`);
|
|
439
|
+
|
|
395
440
|
if (!isUserReadyForOrders()) {
|
|
396
|
-
requireAuthAndOferta(() =>
|
|
397
|
-
companyName: $('#inpPakOrg')?.value.trim(),
|
|
398
|
-
inn: $('#inpPakInn')?.value.trim(),
|
|
399
|
-
email: $('#inpPakEmail')?.value.trim(),
|
|
400
|
-
});
|
|
441
|
+
requireAuthAndOferta(() => buyClientConfig(), { intentTitle });
|
|
401
442
|
return;
|
|
402
443
|
}
|
|
403
|
-
const variant = $('#segPakVariant button.on')?.dataset.variant || 'pak';
|
|
404
|
-
const isLicenseOnly = variant === 'license_only';
|
|
405
444
|
const st = $('#pakOrderStatus');
|
|
406
|
-
st.textContent =
|
|
445
|
+
if (st) st.textContent = 'Добавляем заказ в ваш Личный кабинет…';
|
|
407
446
|
try {
|
|
408
|
-
const data = await api('/api/orders/
|
|
447
|
+
const data = await api('/api/cabinet/orders/draft', {
|
|
409
448
|
sessionKey: currentUser?.sessionKey || '',
|
|
410
|
-
includeBasePak
|
|
411
|
-
includeBaseLicenseOnly
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
years: 1,
|
|
416
|
-
customer: { name: $('#inpPakOrg').value.trim(), inn: $('#inpPakInn').value.trim(), email: $('#inpPakEmail').value.trim() },
|
|
449
|
+
includeBasePak,
|
|
450
|
+
includeBaseLicenseOnly,
|
|
451
|
+
tier,
|
|
452
|
+
addonSeats,
|
|
453
|
+
years,
|
|
417
454
|
});
|
|
418
|
-
if (
|
|
419
|
-
|
|
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>`;
|
|
455
|
+
if (data.user) {
|
|
456
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
457
|
+
onAuthenticated();
|
|
423
458
|
}
|
|
424
|
-
|
|
459
|
+
if (st) st.textContent = '';
|
|
460
|
+
switchStage('addons');
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
if (st) st.textContent = err.message;
|
|
425
464
|
}
|
|
426
|
-
catch (err) { st.textContent = err.message; }
|
|
427
465
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
466
|
+
|
|
467
|
+
async function payCabinetOrder(orderId, paymentMethod = 'yookassa') {
|
|
468
|
+
if (!orderId) return;
|
|
469
|
+
let companyName = '';
|
|
470
|
+
let inn = '';
|
|
471
|
+
if (paymentMethod === 'invoice') {
|
|
472
|
+
companyName = $(`#invCompany-${orderId}`)?.value.trim() || currentUser?.companyName || '';
|
|
473
|
+
inn = $(`#invInn-${orderId}`)?.value.trim() || currentUser?.inn || '';
|
|
474
|
+
if (!companyName || !inn) {
|
|
475
|
+
const invBox = $(`#invBox-${orderId}`);
|
|
476
|
+
if (invBox) invBox.style.display = '';
|
|
477
|
+
$(`#invCompany-${orderId}`)?.focus();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
432
480
|
}
|
|
433
|
-
const st = $('#orderStatus');
|
|
434
|
-
st.textContent = paymentMethod === 'invoice' ? 'Формируем счёт-договор для юр. лица…' : 'Формируем счёт ЮKassa…';
|
|
435
481
|
try {
|
|
436
|
-
const data = await api('/api/orders/
|
|
482
|
+
const data = await api('/api/cabinet/orders/pay', {
|
|
437
483
|
sessionKey: currentUser?.sessionKey || '',
|
|
438
|
-
|
|
484
|
+
orderId,
|
|
439
485
|
paymentMethod,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
years: Number($('#inpYears').value) || 1,
|
|
443
|
-
licenseId: $('#inpLicId').value.trim(),
|
|
444
|
-
customer: { name: currentUser?.companyName || '', inn: currentUser?.inn || '', email: currentUser?.email || '' },
|
|
486
|
+
companyName,
|
|
487
|
+
inn,
|
|
445
488
|
});
|
|
446
|
-
if (
|
|
447
|
-
|
|
489
|
+
if (data.user) {
|
|
490
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
491
|
+
onAuthenticated();
|
|
448
492
|
}
|
|
449
|
-
|
|
450
|
-
|
|
493
|
+
const url = data.invoiceUrl || data.confirmationUrl;
|
|
494
|
+
if (url) window.open(url, '_blank', 'noopener');
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
alert(err.message);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function cancelCabinetOrder(orderId) {
|
|
502
|
+
if (!orderId) return;
|
|
503
|
+
try {
|
|
504
|
+
const data = await api('/api/cabinet/orders/cancel', {
|
|
505
|
+
sessionKey: currentUser?.sessionKey || '',
|
|
506
|
+
orderId,
|
|
507
|
+
});
|
|
508
|
+
if (data.user) {
|
|
509
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
510
|
+
onAuthenticated();
|
|
451
511
|
}
|
|
452
512
|
refreshCatalogRegions();
|
|
453
513
|
}
|
|
454
|
-
catch (err) {
|
|
514
|
+
catch (err) {
|
|
515
|
+
alert(err.message);
|
|
516
|
+
}
|
|
455
517
|
}
|
|
518
|
+
|
|
456
519
|
async function generateCabinetToken() {
|
|
457
520
|
if (!currentUser?.sessionKey) return;
|
|
458
521
|
try {
|
|
@@ -493,7 +556,7 @@
|
|
|
493
556
|
];
|
|
494
557
|
let i = 0;
|
|
495
558
|
const next = () => {
|
|
496
|
-
if (currentStage !== '
|
|
559
|
+
if (currentStage !== 'product' || i >= script.length) return;
|
|
497
560
|
const [who, text, tools] = script[i++];
|
|
498
561
|
const el = document.createElement('div');
|
|
499
562
|
if (who === 'v') {
|
|
@@ -842,32 +905,33 @@
|
|
|
842
905
|
code = code || selectedCode;
|
|
843
906
|
if (!code) return;
|
|
844
907
|
selectRegion(code);
|
|
908
|
+
const reg = REGIONS.find(r => codeOf(r) === code);
|
|
909
|
+
const regTitle = reg ? nameOf(reg) : code;
|
|
845
910
|
if (!isUserReadyForOrders()) {
|
|
846
911
|
requireAuthAndOferta(() => reserveRegionFranchise(code), {
|
|
847
|
-
|
|
848
|
-
inn: $('#inpPartnerInn')?.value.trim(),
|
|
849
|
-
email: $('#inpPartnerEmail')?.value.trim(),
|
|
912
|
+
intentTitle: `Резервирование франшизы (${regTitle})`,
|
|
850
913
|
});
|
|
851
914
|
return;
|
|
852
915
|
}
|
|
853
916
|
const msg = $('#partnerActionMsg');
|
|
854
|
-
msg.textContent = '
|
|
917
|
+
if (msg) msg.textContent = 'Добавляем резерв франшизы в ваш Личный кабинет…';
|
|
855
918
|
try {
|
|
856
|
-
const data = await api('/api/
|
|
919
|
+
const data = await api('/api/cabinet/orders/draft', {
|
|
857
920
|
sessionKey: currentUser?.sessionKey || '',
|
|
921
|
+
kind: 'franchise_reservation',
|
|
858
922
|
regionCode: code,
|
|
859
|
-
companyName: $('#inpPartnerCompany').value.trim(),
|
|
860
|
-
inn: $('#inpPartnerInn').value.trim(),
|
|
861
|
-
email: $('#inpPartnerEmail').value.trim(),
|
|
862
923
|
});
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
924
|
+
if (data.user) {
|
|
925
|
+
currentUser = { ...data.user, sessionKey: currentUser.sessionKey };
|
|
926
|
+
onAuthenticated();
|
|
927
|
+
}
|
|
928
|
+
if (msg) msg.textContent = '';
|
|
868
929
|
refreshCatalogRegions();
|
|
930
|
+
switchStage('addons');
|
|
931
|
+
}
|
|
932
|
+
catch (err) {
|
|
933
|
+
if (msg) msg.textContent = err.message;
|
|
869
934
|
}
|
|
870
|
-
catch (err) { msg.textContent = err.message; }
|
|
871
935
|
}
|
|
872
936
|
window.reserveRegionFranchise = reserveRegionFranchise;
|
|
873
937
|
|
|
@@ -1123,9 +1187,17 @@
|
|
|
1123
1187
|
/* ================= Привязка событий ================= */
|
|
1124
1188
|
function bind() {
|
|
1125
1189
|
document.addEventListener('click', (e) => {
|
|
1126
|
-
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-
|
|
1190
|
+
const el = e.target.closest('[data-stage],[data-action],[data-reserve],[data-pay-order],[data-invoice-toggle],[data-cancel-order],[data-ask],[data-district],th[data-sort],#regionsTableBody tr');
|
|
1127
1191
|
if (!el) return;
|
|
1128
1192
|
if (el.matches('[data-reserve]')) { e.stopPropagation(); reserveRegionFranchise(el.dataset.reserve); return; }
|
|
1193
|
+
if (el.matches('[data-pay-order]')) { e.stopPropagation(); payCabinetOrder(el.dataset.payOrder, el.dataset.payMethod || 'yookassa'); return; }
|
|
1194
|
+
if (el.matches('[data-invoice-toggle]')) {
|
|
1195
|
+
e.stopPropagation();
|
|
1196
|
+
const box = $(`#invBox-${el.dataset.invoiceToggle}`);
|
|
1197
|
+
if (box) box.style.display = box.style.display === 'none' ? '' : 'none';
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (el.matches('[data-cancel-order]')) { e.stopPropagation(); cancelCabinetOrder(el.dataset.cancelOrder); return; }
|
|
1129
1201
|
if (el.matches('#regionsTableBody tr')) { selectRegion(el.dataset.code, { zoom: true }); $('#mapBox').scrollIntoView({ block: 'nearest', behavior: 'smooth' }); return; }
|
|
1130
1202
|
if (el.matches('th[data-sort]')) {
|
|
1131
1203
|
SORT.dir = SORT.key === el.dataset.sort ? -SORT.dir : (el.dataset.sort === 'name' || el.dataset.sort === 'district' ? 1 : -1);
|
|
@@ -1134,14 +1206,22 @@
|
|
|
1134
1206
|
return;
|
|
1135
1207
|
}
|
|
1136
1208
|
if (el.matches('[data-district]')) { setDistrict(el.dataset.district); return; }
|
|
1137
|
-
if (el.matches('[data-bot]')) { $('#botText').value = el.dataset.bot; sendBot(); return; }
|
|
1138
1209
|
if (el.matches('[data-ask]')) { sendAi(el.dataset.ask); return; }
|
|
1139
1210
|
if (el.dataset.stage) { e.preventDefault(); switchStage(el.dataset.stage); return; }
|
|
1140
1211
|
switch (el.dataset.action) {
|
|
1141
|
-
case 'auth':
|
|
1212
|
+
case 'auth':
|
|
1213
|
+
if (isUserReadyForOrders() && currentStage !== 'addons') {
|
|
1214
|
+
switchStage('addons');
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
pendingAuthAction = null;
|
|
1218
|
+
openAuth();
|
|
1219
|
+
}
|
|
1220
|
+
break;
|
|
1142
1221
|
case 'auth-close': closeAuth(); break;
|
|
1143
|
-
case 'login': performLogin(); break;
|
|
1144
|
-
case '
|
|
1222
|
+
case 'login': performLogin('email'); break;
|
|
1223
|
+
case 'oauth-login': performLogin(el.dataset.provider || 'email'); break;
|
|
1224
|
+
case 'logout': e.preventDefault(); performLogout(); break;
|
|
1145
1225
|
case 'open-oferta': e.preventDefault(); openOfertaModal(); break;
|
|
1146
1226
|
case 'oferta-close': closeOfertaModal(); break;
|
|
1147
1227
|
case 'accept-from-oferta':
|
|
@@ -1155,18 +1235,18 @@
|
|
|
1155
1235
|
else requireAuthAndOferta(() => switchStage('addons'));
|
|
1156
1236
|
break;
|
|
1157
1237
|
case 'pick-tier':
|
|
1158
|
-
$$('#
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1238
|
+
$$('#segPakTier button').forEach(b => b.classList.toggle('on', b.dataset.tier === el.dataset.tier));
|
|
1239
|
+
if ($('#segPakVariant')) {
|
|
1240
|
+
$$('#segPakVariant button').forEach(b => b.classList.toggle('on', b.dataset.variant === 'addons_only'));
|
|
1241
|
+
}
|
|
1242
|
+
syncClientSeats(5);
|
|
1243
|
+
buyClientConfig();
|
|
1162
1244
|
break;
|
|
1163
|
-
case '
|
|
1164
|
-
case 'order-pak-invoice': submitBasePakOrder('invoice'); break;
|
|
1165
|
-
case 'order-addons': submitAddonOrder('yookassa'); break;
|
|
1166
|
-
case 'order-addons-invoice': submitAddonOrder('invoice'); break;
|
|
1245
|
+
case 'buy-client-config': buyClientConfig(); break;
|
|
1167
1246
|
case 'gen-hub-token': generateCabinetToken(); break;
|
|
1247
|
+
case 'copy-hub-token': copyCabinetToken(); break;
|
|
1248
|
+
case 'refresh-cabinet': refreshCabinetUser(); break;
|
|
1168
1249
|
case 'reserve-selected': reserveRegionFranchise(selectedCode); break;
|
|
1169
|
-
case 'bot-send': sendBot(); break;
|
|
1170
1250
|
case 'cert-close': $('#certModal')?.classList.remove('open'); break;
|
|
1171
1251
|
case 'cert-copy':
|
|
1172
1252
|
navigator.clipboard?.writeText($('#certJsonBox')?.textContent || '');
|
|
@@ -1181,18 +1261,20 @@
|
|
|
1181
1261
|
document.addEventListener('keydown', (e) => {
|
|
1182
1262
|
if (e.key === 'Escape') { closeAuth(); closeOfertaModal(); $('#certModal')?.classList.remove('open'); toggleAi(false); }
|
|
1183
1263
|
});
|
|
1184
|
-
$('#authModal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') performLogin(); });
|
|
1264
|
+
$('#authModal')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') performLogin('email'); });
|
|
1185
1265
|
$('#aiInput')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendAi(); });
|
|
1186
|
-
$('#
|
|
1187
|
-
$('#
|
|
1188
|
-
$('#
|
|
1189
|
-
$('#inpYears')?.addEventListener('input', recalcAddons);
|
|
1190
|
-
$('#inpPakExtraSeats')?.addEventListener('input', recalcBaseOrder);
|
|
1266
|
+
$('#inpPakExtraSeats')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
|
|
1267
|
+
$('#inpPakExtraSeatsNum')?.addEventListener('input', (e) => syncClientSeats(e.target.value));
|
|
1268
|
+
$('#inpPakYears')?.addEventListener('input', recalcBaseOrder);
|
|
1191
1269
|
$('#rngPartnerSales')?.addEventListener('input', recalcPartner);
|
|
1192
1270
|
$('#inpRegionFilter')?.addEventListener('input', renderRegionsTable);
|
|
1193
|
-
bindSeg('segTier', recalcAddons);
|
|
1194
1271
|
bindSeg('segPakTier', recalcBaseOrder);
|
|
1195
|
-
bindSeg('segPakVariant',
|
|
1272
|
+
bindSeg('segPakVariant', () => syncClientSeats($('#inpPakExtraSeatsNum')?.value || 0));
|
|
1273
|
+
window.addEventListener('message', (e) => {
|
|
1274
|
+
if (e.data?.type === 'odant:order-paid') {
|
|
1275
|
+
refreshCabinetUser();
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1196
1278
|
window.addEventListener('resize', moveIndicator);
|
|
1197
1279
|
window.addEventListener('hashchange', () => { const id = location.hash.slice(1); if (id && id !== currentStage) switchStage(id, { scroll: false }); });
|
|
1198
1280
|
}
|
|
@@ -1201,7 +1283,6 @@
|
|
|
1201
1283
|
applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
|
|
1202
1284
|
bind();
|
|
1203
1285
|
renderOrbit();
|
|
1204
|
-
recalcAddons();
|
|
1205
1286
|
recalcBaseOrder();
|
|
1206
1287
|
recalcPartner();
|
|
1207
1288
|
renderRegionsTable();
|