const state = { user: 'admin', role: 'admin', token: null, authSource: 'local', fullName: null, dashboard: null, leads: [], deals: [], selectedDealId: '', workspaceById: {}, logLines: [], }; const SESSION_STORAGE_KEY = 'cc_session'; const STAGE_OPTIONS = [ 'new_qualified_lead', 'warm_lead', 'hot_lead', 'enrichment_required', 'active_text_communication', 'active_voice_communication', 'waiting_customer_reply', 'offer_preparing', 'offer_sent', 'conditions_negotiation', 'counterparty_data_requested', 'counterparty_data_received', 'document_preparing', 'document_sent', 'document_confirmed', 'invoice_preparing', 'invoice_sent', 'payment_expected', 'partially_paid', 'paid', 'payment_overdue', 'won', 'lost', 'postponed', 'follow_up_scheduled', 'transferred_to_execution', 'transferred_to_support', ]; const BOARD_GROUPS = [ { key: 'inbound', title: 'Вход', note: 'Квалифицированный lead intake и дообогащение.', stages: ['new_qualified_lead', 'warm_lead', 'hot_lead', 'enrichment_required'] }, { key: 'communication', title: 'Коммуникация', note: 'Text/voice сессии, вопросы и follow-up.', stages: ['active_text_communication', 'active_voice_communication', 'waiting_customer_reply'] }, { key: 'commercial', title: 'Коммерция', note: 'Оффер, согласование условий, next step.', stages: ['offer_preparing', 'offer_sent', 'conditions_negotiation'] }, { key: 'paperwork', title: 'Оформление', note: 'Реквизиты, документы, подтверждение.', stages: ['counterparty_data_requested', 'counterparty_data_received', 'document_preparing', 'document_sent', 'document_confirmed'] }, { key: 'finance', title: 'Финансы', note: 'Счет, оплата, просрочка.', stages: ['invoice_preparing', 'invoice_sent', 'payment_expected', 'partially_paid', 'paid', 'payment_overdue'] }, { key: 'outcome', title: 'Закрытие', note: 'Win/loss/post-sale handoff.', stages: ['won', 'lost', 'postponed', 'follow_up_scheduled', 'transferred_to_execution', 'transferred_to_support'] }, ]; const STAGE_LABELS = { new_qualified_lead: 'Новый квалифицированный лид', warm_lead: 'Теплый лид', hot_lead: 'Горячий лид', enrichment_required: 'Нужно дообогащение', active_text_communication: 'Текстовая коммуникация', active_voice_communication: 'Голосовая коммуникация', waiting_customer_reply: 'Ждем ответ клиента', offer_preparing: 'Готовим оффер', offer_sent: 'Оффер отправлен', conditions_negotiation: 'Согласуем условия', counterparty_data_requested: 'Запрос реквизитов', counterparty_data_received: 'Реквизиты получены', document_preparing: 'Готовим документ', document_sent: 'Документ отправлен', document_confirmed: 'Документ подтвержден', invoice_preparing: 'Готовим счет', invoice_sent: 'Счет отправлен', payment_expected: 'Ждем оплату', partially_paid: 'Частичная оплата', paid: 'Оплачено', payment_overdue: 'Просрочка оплаты', won: 'Успешно', lost: 'Потеряно', postponed: 'Отложено', follow_up_scheduled: 'Follow-up', transferred_to_execution: 'Передано в исполнение', transferred_to_support: 'Передано человеку', }; const $ = (id) => document.getElementById(id); function log(message, payload) { const ts = new Date().toLocaleTimeString(); const suffix = payload ? ` ${JSON.stringify(payload)}` : ''; state.logLines.unshift(`[${ts}] ${message}${suffix}`); state.logLines = state.logLines.slice(0, 14); $('salesLog').textContent = state.logLines.join('\n'); } function syncSessionFromInputs() { state.user = $('sessionUser').value.trim() || 'admin'; state.role = $('sessionRole').value || 'admin'; } function persistSession() { window.localStorage.setItem( SESSION_STORAGE_KEY, JSON.stringify({ access_token: state.token, user: state.user, role: state.role, auth_source: state.authSource, full_name: state.fullName, }), ); } function clearStoredSession() { window.localStorage.removeItem(SESSION_STORAGE_KEY); } function restoreStoredSession() { const raw = window.localStorage.getItem(SESSION_STORAGE_KEY); if (!raw) { return false; } try { const payload = JSON.parse(raw); if (!payload?.access_token) { clearStoredSession(); return false; } state.token = payload.access_token; state.user = payload.user || state.user; state.role = payload.role || state.role; state.authSource = payload.auth_source || 'local'; state.fullName = payload.full_name || null; $('sessionUser').value = state.user; $('sessionRole').value = state.role; $('authPanel').style.display = 'none'; updateSessionInfo(); updateProfile(); return true; } catch { clearStoredSession(); return false; } } function updateSessionInfo(extra = '') { $('sessionInfo').textContent = state.token ? `Токен активен | роль: ${state.role}${extra ? ` | ${extra}` : ''}` : 'Токен: отсутствует'; } function updateProfile() { $('profileName').textContent = state.fullName || state.user || 'Экран продаж'; $('profileRole').textContent = `Роль: ${state.role || 'unknown'}`; } async function api(path, options = {}) { syncSessionFromInputs(); const headers = { 'Content-Type': 'application/json', 'X-User': state.user, 'X-Role': state.role, ...(options.headers || {}), }; if (state.token) { headers.Authorization = `Bearer ${state.token}`; } const response = await fetch(`/proxy/sales/${path}`, { ...options, headers }); let data; try { data = await response.json(); } catch { data = { error: 'Сервис вернул не JSON' }; } if (!response.ok) { throw new Error(`${response.status}: ${JSON.stringify(data)}`); } return data; } async function login() { syncSessionFromInputs(); try { const data = await fetch('/proxy/auth/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': state.user, 'X-Role': state.role, }, body: JSON.stringify({ username: state.user, password: $('sessionPassword').value, }), }).then((response) => response.json()); state.token = data.access_token; state.role = data.role; state.authSource = data.auth_source || 'local'; state.fullName = data.full_name || null; $('authPanel').style.display = 'none'; updateSessionInfo(); updateProfile(); persistSession(); log('Логин выполнен', { user: state.user, role: state.role }); await refreshAll(); } catch (error) { updateSessionInfo(`ошибка: ${error.message}`); log('Ошибка входа', { error: error.message }); } } function logout() { clearStoredSession(); window.location.href = '/'; } async function checkGateway() { try { const data = await fetch('/health').then((response) => response.json()); $('gatewayStatus').textContent = `Шлюз: ${data.status === 'ok' ? 'готов' : data.status}`; } catch { $('gatewayStatus').textContent = 'Шлюз: недоступен'; } } function stageLabel(stageId) { return STAGE_LABELS[stageId] || stageId || 'Без этапа'; } function boardGroupsWithItems() { const query = ($('salesSearch').value || '').trim().toLowerCase(); return BOARD_GROUPS.map((group) => { const items = state.deals.filter((deal) => { const inGroup = group.stages.includes(deal.stage_id); if (!inGroup) { return false; } if (!query) { return true; } return [ deal.title, deal.need_summary, deal.current_channel, deal.customer_id, ].some((value) => String(value || '').toLowerCase().includes(query)); }); return { ...group, items }; }); } function salesCard(deal) { const activeClass = deal.deal_id === state.selectedDealId ? 'active' : ''; const amount = Number(deal.final_amount || deal.estimated_amount || 0); return ` `; } function renderBoard() { $('salesBoard').innerHTML = boardGroupsWithItems() .map( (group) => `
${group.title}
${group.items.length}
${group.note}
${group.items.length ? group.items.map((deal) => salesCard(deal)).join('') : '
Пусто.
'}
`, ) .join(''); } function kpiCard(label, value, note) { return `
${label}
${value}
${note}
`; } function renderDashboard() { const data = state.dashboard; if (!data) { $('salesDashboard').innerHTML = ''; return; } $('salesDashboard').innerHTML = [ kpiCard('Лиды', Number(data.leads_total || 0), 'Сколько квалифицированных лидов уже в CRM'), kpiCard('Активные сделки', Number(data.deals_active || 0), 'Открытые кейсы в работе'), kpiCard('Выиграно', Number(data.deals_won || 0), 'Закрыто с оплатой'), kpiCard('Просрочка', Number(data.overdue_invoices || 0), 'Счета, которые зависли'), kpiCard('Ожидаем оплату', `${Number(data.payment_expected || 0).toLocaleString('ru-RU')} KZT`, 'Потенциальный cash-in'), kpiCard('Получено', `${Number(data.payment_received || 0).toLocaleString('ru-RU')} KZT`, 'Фактические оплаты'), kpiCard('Voice сессии', Number(data.voice_sessions || 0), 'Голосовой контур продаж'), kpiCard('Text сессии', Number(data.text_sessions || 0), 'Текстовый контур продаж'), kpiCard('Switch channel', Number(data.channel_switches || 0), 'Переходы между voice/text'), kpiCard('Эскалации', Number(data.human_escalations || 0), 'Передачи живому менеджеру'), ].join(''); } function renderStageOptions() { $('dealStageSelect').innerHTML = STAGE_OPTIONS.map((value) => ``).join(''); } function workspaceMetric(label, value) { return `
${label}
${value}
`; } function kvItem(label, value) { return `
${label}
${value || '—'}
`; } function listRows(items, titleKey, bodyKey) { if (!items?.length) { return '
Пока пусто.
'; } return `
${items .map( (item) => `
${item[titleKey]} ${item[bodyKey] || ''}
${item.updated_at || item.created_at || item.started_at || ''}
`, ) .join('')}
`; } function renderWorkspace() { if (!state.selectedDealId) { $('dealWorkspace').innerHTML = 'Выберите карточку на воронке, чтобы увидеть подробности.'; $('dealMetrics').innerHTML = ''; $('dealTitle').textContent = 'Выберите сделку'; $('dealSubtitle').textContent = 'Справа будет единый контекст: text + voice, офферы, счета, оплата, timeline.'; $('dealStageBadge').textContent = 'Нет данных'; $('telegramThreadId').value = ''; $('telegramChatId').value = ''; $('voiceSessionId').value = ''; return; } const workspace = state.workspaceById[state.selectedDealId]; if (!workspace) { $('dealWorkspace').innerHTML = 'Загружаю карточку сделки...'; return; } const { deal, lead, communications, offers, documents, invoices, payments, timeline, counterparty, escalations, tasks } = workspace; const bindings = workspaceBindings(workspace); $('dealTitle').textContent = deal.title; $('dealSubtitle').textContent = `${stageLabel(deal.stage_id)} • ${deal.current_channel || 'канал не задан'} • ${deal.status}`; $('dealStageBadge').textContent = stageLabel(deal.stage_id); $('dealStageSelect').value = deal.stage_id; $('telegramThreadId').value = bindings.telegramThreadId || ''; $('telegramChatId').value = bindings.telegramChatId || ''; $('voiceSessionId').value = bindings.voiceSessionId || ''; $('dealMetrics').innerHTML = [ workspaceMetric('Канал', deal.current_channel || '—'), workspaceMetric('Следующий шаг', deal.next_action_type || 'не задан'), workspaceMetric('Сумма', `${Number(deal.final_amount || deal.estimated_amount || 0).toLocaleString('ru-RU')} ${deal.currency || 'KZT'}`), workspaceMetric('Customer link', deal.customer_id || 'не привязан'), ].join(''); $('dealWorkspace').innerHTML = `

Lead / Deal core

${kvItem('Lead', lead?.lead_id || '—')} ${kvItem('Имя', lead?.full_name || '—')} ${kvItem('Телефон', lead?.phone || '—')} ${kvItem('Предпочтительный канал', deal.preferred_channel || '—')} ${kvItem('Score', lead?.lead_score || '—')} ${kvItem('Потребность', deal.need_summary || lead?.initial_need_summary || '—')}

Counterparty

${kvItem('Компания', counterparty?.company_name || '—')} ${kvItem('Контакт', counterparty?.full_name || '—')} ${kvItem('BIN/IIN', counterparty?.bin_iin || '—')} ${kvItem('Email для документов', counterparty?.email_for_docs || '—')}

Коммуникации

${listRows( communications.map((item) => ({ ...item, headline: `${item.channel_type === 'voice' ? 'Voice' : 'Text'} • ${item.direction}`, detail: item.summary || item.subject || item.result_code || 'Без summary', })), 'headline', 'detail', )}

Коммерческий пакет

${listRows( offers.map((item) => ({ ...item, headline: `${item.title} • ${item.status}`, detail: `${Number(item.total_amount || 0).toLocaleString('ru-RU')} ${item.currency}`, })), 'headline', 'detail', )}

Документы и счета

${listRows( [...documents, ...invoices].map((item) => ({ ...item, headline: item.document_id ? `${item.document_type} • ${item.status}` : `${item.invoice_number} • ${item.status}`, detail: item.document_id ? (item.file_url || 'Файл еще не прикреплен') : `${Number(item.amount || 0).toLocaleString('ru-RU')} ${item.currency}`, })), 'headline', 'detail', )}

Оплаты, задачи, эскалации

${listRows( [ ...payments.map((item) => ({ ...item, headline: `Оплата • ${item.status}`, detail: `${Number(item.amount || 0).toLocaleString('ru-RU')} ${item.currency}`, })), ...tasks.map((item) => ({ ...item, headline: `Task • ${item.task_type}`, detail: `${item.status} • ${item.run_at}`, })), ...escalations.map((item) => ({ ...item, headline: `Эскалация • ${item.severity}`, detail: item.reason, })), ], 'headline', 'detail', )}

Timeline

${timeline.length ? timeline .map( (item) => `
${item.title}
${item.ts}
${item.body}
`, ) .join('') : '
Пока нет timeline событий.
'}
`; } function latestCommunication(workspace, channelType = '') { const items = workspace?.communications || []; if (!channelType) { return items[0] || null; } return items.find((item) => item.channel_type === channelType) || items[0] || null; } function workspaceBindings(workspace) { const bindings = { telegramThreadId: '', telegramChatId: '', voiceSessionId: '', }; for (const item of workspace?.communications || []) { const metadata = item.metadata || {}; if (!bindings.telegramThreadId && metadata.telegram_thread_id) { bindings.telegramThreadId = metadata.telegram_thread_id; } if (!bindings.telegramChatId && metadata.telegram_chat_id) { bindings.telegramChatId = metadata.telegram_chat_id; } if (!bindings.voiceSessionId && metadata.voice_session_id) { bindings.voiceSessionId = metadata.voice_session_id; } } return bindings; } async function loadDashboard() { state.dashboard = await api('api/v1/dashboard'); renderDashboard(); } async function loadDeals() { const query = ($('salesSearch').value || '').trim(); const params = new URLSearchParams(); if (query) { params.set('query', query); } const path = params.toString() ? `api/v1/deals?${params.toString()}` : 'api/v1/deals'; state.deals = await api(path); renderBoard(); } async function loadLeads() { state.leads = await api('api/v1/leads'); } async function loadWorkspace(dealId) { state.workspaceById[dealId] = await api(`api/v1/deals/${encodeURIComponent(dealId)}/workspace`); renderWorkspace(); } async function refreshAll() { await Promise.all([loadDashboard(), loadDeals(), loadLeads()]); if (state.selectedDealId) { await loadWorkspace(state.selectedDealId); } } async function selectDeal(dealId) { state.selectedDealId = dealId; renderBoard(); renderWorkspace(); await loadWorkspace(dealId); } function selectedWorkspace() { return state.workspaceById[state.selectedDealId] || null; } async function createLead() { try { const payload = { source_type: 'crm', source_channel: $('leadSourceChannel').value, full_name: $('leadName').value.trim(), company_name: $('leadCompany').value.trim() || null, phone: $('leadPhone').value.trim() || null, email: $('leadEmail').value.trim() || null, lead_temperature: $('leadTemperature').value, lead_score: Number($('leadScore').value || 68), initial_need_summary: $('leadNeedSummary').value.trim() || null, preferred_channel: $('leadPreferredChannel').value, assigned_agent_type: $('leadPreferredChannel').value === 'voice' ? 'voice_ai' : 'text_ai', status: 'new_qualified_lead', priority: 3, title: $('leadName').value.trim() ? `Лид: ${$('leadName').value.trim()}` : null, }; await api('api/v1/leads', { method: 'POST', body: JSON.stringify(payload) }); log('Лид создан', payload); await refreshAll(); } catch (error) { log('Ошибка создания лида', { error: error.message }); } } async function seedDemo() { $('leadName').value = 'Demo Buyer'; $('leadCompany').value = 'KazTrade Labs'; $('leadPhone').value = '+77015551234'; $('leadEmail').value = 'buyer@kaztrade.test'; $('leadNeedSummary').value = 'Нужен расчет и оффер на пакет услуг с возможностью быстро перейти из чата в звонок.'; $('leadPreferredChannel').value = 'telegram'; $('leadTemperature').value = 'hot'; $('leadScore').value = '81'; await createLead(); } async function postQuickAction(handler, successLabel) { if (!state.selectedDealId) { log('Сначала выберите сделку.'); return; } try { await handler(); log(successLabel, { deal_id: state.selectedDealId }); await refreshAll(); } catch (error) { log('Ошибка действия', { error: error.message }); } } async function startTextSession() { return postQuickAction( () => api(`api/v1/deals/${encodeURIComponent(state.selectedDealId)}/communications/text`, { method: 'POST', body: JSON.stringify({ channel_type: 'text', direction: 'outbound', agent_type: 'text_ai', subject: 'Текстовый follow-up по сделке', }), }), 'Текстовая сессия создана', ); } async function startVoiceSession() { const workspace = selectedWorkspace(); const phone = workspace?.lead?.phone || workspace?.counterparty?.phone_for_docs || '+77010000000'; return postQuickAction( () => api('api/v1/calls/outbound', { method: 'POST', body: JSON.stringify({ deal_id: state.selectedDealId, phone_number: phone, provider: 'voice_assistant', subject: 'Голосовой follow-up по сделке', }), }), 'Голосовая сессия создана', ); } async function switchToText() { const workspace = selectedWorkspace(); const communication = workspace?.communications?.[0]; if (!communication) { log('Нет коммуникации для channel switch.'); return; } return postQuickAction( () => api(`api/v1/communications/${encodeURIComponent(communication.communication_id)}/switch-channel`, { method: 'POST', body: JSON.stringify({ to_channel: 'telegram', reason_for_channel_switch: 'После звонка нужно зафиксировать условия письменно', }), }), 'Сделка переведена в текст', ); } async function switchToVoice() { const workspace = selectedWorkspace(); const communication = workspace?.communications?.[0]; if (!communication) { log('Нет коммуникации для channel switch.'); return; } return postQuickAction( () => api(`api/v1/communications/${encodeURIComponent(communication.communication_id)}/switch-channel`, { method: 'POST', body: JSON.stringify({ to_channel: 'voice', reason_for_channel_switch: 'Сложный кейс, удобнее перейти в звонок', }), }), 'Сделка переведена в voice', ); } async function createOffer() { const workspace = selectedWorkspace(); const amount = Number(workspace?.deal?.estimated_amount || workspace?.deal?.final_amount || 150000); return postQuickAction( () => api(`api/v1/deals/${encodeURIComponent(state.selectedDealId)}/offers`, { method: 'POST', body: JSON.stringify({ offer_type: 'offer', title: `Коммерческое предложение • ${workspace?.deal?.title || 'Сделка'}`, description: 'Автогенерированный оффер для быстрого запуска продажного цикла.', line_items: [{ name: 'Основной пакет', quantity: 1, price: amount }], pricing: { subtotal: amount, vat: 0, total: amount }, total_amount: amount, currency: workspace?.deal?.currency || 'KZT', }), }), 'Оффер создан', ); } async function createInvoice() { const workspace = selectedWorkspace(); const amount = Number(workspace?.deal?.final_amount || workspace?.deal?.estimated_amount || 150000); const firstDocument = workspace?.documents?.[0]; return postQuickAction( () => api(`api/v1/deals/${encodeURIComponent(state.selectedDealId)}/invoices`, { method: 'POST', body: JSON.stringify({ basis_document_id: firstDocument?.document_id || null, amount, currency: workspace?.deal?.currency || 'KZT', due_date: $('nextActionAt').value.trim() || null, line_items: [{ title: workspace?.deal?.title || 'Сделка', amount }], metadata: { generated_from: 'sales_ui' }, }), }), 'Счет создан', ); } async function markPaid() { const workspace = selectedWorkspace(); const invoice = workspace?.invoices?.[0]; if (!invoice) { log('Сначала создайте счет.'); return; } return postQuickAction( () => api('api/v1/payments/webhook', { method: 'POST', body: JSON.stringify({ deal_id: state.selectedDealId, invoice_id: invoice.invoice_id, payment_provider: 'manual', amount: invoice.amount, currency: invoice.currency, status: 'success', payment_method: 'bank_transfer', }), }), 'Оплата зафиксирована', ); } async function saveStage() { return postQuickAction( () => api(`api/v1/deals/${encodeURIComponent(state.selectedDealId)}/change-stage`, { method: 'POST', body: JSON.stringify({ stage_id: $('dealStageSelect').value, reason: 'Ручное обновление стадии из sales workspace', }), }), 'Этап обновлен', ); } async function saveNextAction() { return postQuickAction( () => api(`api/v1/deals/${encodeURIComponent(state.selectedDealId)}/schedule-next-action`, { method: 'POST', body: JSON.stringify({ next_action_type: $('nextActionType').value.trim() || 'follow_up', next_action_at: $('nextActionAt').value.trim() || null, payload: { source: 'sales_ui' }, }), }), 'Next action сохранен', ); } async function bindTelegramThread() { const workspace = selectedWorkspace(); const communication = latestCommunication(workspace, 'text'); const threadId = $('telegramThreadId').value.trim(); if (!communication) { log('Сначала откройте text-сессию по сделке.'); return; } if (!threadId) { log('Укажите telegram thread id.'); return; } return postQuickAction( () => api(`api/v1/communications/${encodeURIComponent(communication.communication_id)}/bind-external`, { method: 'POST', body: JSON.stringify({ channel_provider: 'telegram', telegram_thread_id: threadId, telegram_chat_id: $('telegramChatId').value.trim() || null, metadata: { source: 'sales_ui_binding' }, }), }), 'Telegram thread привязан к сделке', ); } async function bindVoiceSession() { const workspace = selectedWorkspace(); const communication = latestCommunication(workspace, 'voice'); const voiceSessionId = $('voiceSessionId').value.trim(); if (!communication) { log('Сначала откройте voice-сессию по сделке.'); return; } if (!voiceSessionId) { log('Укажите voice session id.'); return; } return postQuickAction( () => api(`api/v1/communications/${encodeURIComponent(communication.communication_id)}/bind-external`, { method: 'POST', body: JSON.stringify({ channel_provider: 'voice', voice_session_id: voiceSessionId, metadata: { source: 'sales_ui_binding' }, }), }), 'Voice session привязана к сделке', ); } async function sendTelegramReply() { const workspace = selectedWorkspace(); const communication = latestCommunication(workspace, 'text'); const threadId = $('telegramThreadId').value.trim(); const body = $('telegramReplyText').value.trim(); if (!threadId) { log('Сначала привяжите Telegram thread.'); return; } if (!body) { log('Введите текст сообщения.'); return; } return postQuickAction( () => api('api/v1/messages/outbound', { method: 'POST', body: JSON.stringify({ deal_id: state.selectedDealId, communication_id: communication?.communication_id || null, channel_provider: 'telegram', sender_type: 'human', sender_id: state.user, body, metadata: { telegram_thread_id: threadId, telegram_chat_id: $('telegramChatId').value.trim() || null, source: 'sales_ui_temp_telegram', }, }), }), 'Сообщение поставлено в Telegram bridge', ).then(() => { $('telegramReplyText').value = ''; }); } function handleBoardClick(event) { const button = event.target.closest('[data-deal-id]'); if (!button) { return; } selectDeal(button.dataset.dealId).catch((error) => { log('Ошибка загрузки сделки', { error: error.message }); }); } function bindEvents() { $('loginBtn').addEventListener('click', login); $('logoutBtn').addEventListener('click', logout); $('refreshSalesBtn').addEventListener('click', () => refreshAll().catch((error) => log('Ошибка обновления', { error: error.message }))); $('salesSearch').addEventListener('input', renderBoard); $('salesBoard').addEventListener('click', handleBoardClick); $('createLeadBtn').addEventListener('click', createLead); $('seedDemoBtn').addEventListener('click', seedDemo); $('startTextBtn').addEventListener('click', startTextSession); $('startVoiceBtn').addEventListener('click', startVoiceSession); $('switchToTextBtn').addEventListener('click', switchToText); $('switchToVoiceBtn').addEventListener('click', switchToVoice); $('createOfferBtn').addEventListener('click', createOffer); $('createInvoiceBtn').addEventListener('click', createInvoice); $('markPaidBtn').addEventListener('click', markPaid); $('saveStageBtn').addEventListener('click', saveStage); $('saveNextActionBtn').addEventListener('click', saveNextAction); $('bindTelegramBtn').addEventListener('click', bindTelegramThread); $('bindVoiceSessionBtn').addEventListener('click', bindVoiceSession); $('sendTelegramReplyBtn').addEventListener('click', sendTelegramReply); } async function init() { renderStageOptions(); bindEvents(); updateProfile(); await checkGateway(); if (restoreStoredSession()) { log('Сессия восстановлена', { user: state.user, role: state.role }); await refreshAll(); } } init().catch((error) => { log('Ошибка инициализации', { error: error.message }); });