const state = { user: 'admin', role: 'admin', token: null, authSource: 'local', fullName: null, logLines: [], oidc: { enabled: false, loginPath: '/auth/oidc/start?return_mode=popup', providerLabel: 'Keycloak', }, voiceNameConfig: null, voiceTtsConfig: null, }; const $ = (id) => document.getElementById(id); const SESSION_STORAGE_KEY = 'cc_session'; const ROLE_LABELS = { admin: 'Администратор', supervisor: 'Супервизор', operator: 'Оператор', analyst: 'Аналитик', }; function destinationForRole(role) { if (role === 'admin') { return '/admin'; } if (role === 'supervisor') { return '/supervisor'; } if (role === 'analyst') { return '/analyst'; } return '/operator'; } function applyRoleNavigation() { document.querySelectorAll('[data-shell]').forEach((link) => { const allowed = (link.dataset.roles || '') .split(',') .map((item) => item.trim()) .filter(Boolean); link.style.display = !allowed.length || allowed.includes(state.role) ? '' : 'none'; }); } function ensurePageAccess() { if (state.role !== 'admin') { window.location.href = destinationForRole(state.role); return false; } return true; } 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; if ($('authPanel')) { $('authPanel').style.display = 'none'; } updateProfileMeta(); applyRoleNavigation(); return true; } catch { clearStoredSession(); return false; } } function logout() { clearStoredSession(); window.location.href = '/'; } function syncSessionFromInputs() { state.user = $('sessionUser').value.trim() || 'admin'; state.role = $('sessionRole').value || 'admin'; } 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, 16); $('eventLog').textContent = state.logLines.join('\n'); } function updateProfileMeta() { const profileName = $('profileName'); const profileRole = $('profileRole'); if (!profileName || !profileRole) { return; } profileName.textContent = state.fullName || state.user || 'Экран администратора'; profileRole.textContent = ROLE_LABELS[state.role] || state.role || 'Пользователь'; } function updateSessionInfo(extra = '') { if (!state.token) { $('sessionInfo').textContent = 'Токен: отсутствует'; return; } const source = state.authSource === 'oidc' ? 'корпоративный' : 'локальный'; $('sessionInfo').textContent = `Токен активен | роль: ${state.role} | вход: ${source}${extra ? ` | ${extra}` : ''}`; } function applyAuthenticatedSession(data) { state.token = data.access_token; state.role = data.role; state.user = data.username || state.user; state.authSource = data.auth_source || 'local'; state.fullName = data.full_name || null; $('sessionUser').value = state.user; $('sessionRole').value = data.role; updateSessionInfo(data.provider ? `провайдер: ${data.provider}` : ''); updateProfileMeta(); persistSession(); applyRoleNavigation(); } async function api(service, 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/${service}/${path}`, { ...options, headers }); let data; try { data = await response.json(); } catch { data = { error: 'Сервис вернул ответ не в JSON-формате' }; } if (!response.ok) { if (response.status === 401 && state.token) { state.token = null; state.authSource = 'local'; clearStoredSession(); updateSessionInfo('требуется повторный вход'); window.location.href = '/'; } throw new Error(`${response.status}: ${JSON.stringify(data)}`); } return data; } async function checkGateway() { try { const response = await fetch('/health'); const data = await response.json(); $('gatewayStatus').textContent = `Шлюз: ${data.status === 'ok' ? 'готов' : data.status}`; } catch { $('gatewayStatus').textContent = 'Шлюз: недоступен'; } } async function loadOidcConfig() { try { const response = await fetch('/proxy/auth/auth/oidc/config'); const data = await response.json(); state.oidc.enabled = Boolean(data.enabled); state.oidc.loginPath = data.login_path || '/auth/oidc/start?return_mode=popup'; state.oidc.providerLabel = data.provider_label || 'Keycloak'; $('corporateLoginBtn').disabled = !state.oidc.enabled; $('corporateLoginBtn').textContent = state.oidc.enabled ? `Корпоративный вход (${state.oidc.providerLabel})` : 'Корпоративный вход недоступен'; } catch { $('corporateLoginBtn').disabled = true; $('corporateLoginBtn').textContent = 'Корпоративный вход недоступен'; } } function startCorporateLogin() { if (!state.oidc.enabled) { log('Корпоративный вход недоступен'); return; } const popup = window.open(`/proxy/auth${state.oidc.loginPath}`, 'oidc-login', 'width=620,height=760'); if (!popup) { log('Не удалось открыть окно корпоративного входа'); return; } log('Открыт корпоративный вход', { провайдер: state.oidc.providerLabel }); } function handleOidcMessage(event) { if (!event?.data || typeof event.data !== 'object') { return; } if (event.data.type === 'oidc-login' && event.data.access_token) { applyAuthenticatedSession(event.data); log('Корпоративный вход выполнен', { user: state.user, role: state.role }); return; } if (event.data.type === 'oidc-error') { log('Корпоративный вход не выполнен', { error: event.data.message || 'неизвестно' }); updateSessionInfo(`ошибка SSO: ${event.data.message || 'неизвестно'}`); } } async function login() { syncSessionFromInputs(); try { const data = await api('auth', 'auth/login', { method: 'POST', body: JSON.stringify({ username: state.user, password: $('sessionPassword').value, }), }); applyAuthenticatedSession({ ...data, username: state.user, auth_source: data.auth_source || 'local' }); log('Локальный вход выполнен', { user: state.user, role: data.role }); } catch (err) { $('sessionInfo').textContent = `Ошибка входа: ${err.message}`; log('Вход не выполнен', { error: err.message }); } } function queueRulePayload(channelId, slaSeconds) { return [ { channel: channelId, priority: 3, strategy: 'round_robin', sla_seconds: Number(slaSeconds || 30), }, ]; } function defaultIvrFlowDocument() { return { nodes: [ { node_id: 'root', prompt_text: 'Здравствуйте! Добро пожаловать в контакт-центр. Для русского языка нажмите цифру два. Саламатсыз ба! Қазақ тілін таңдау үшін бір цифрын теріңіз.', prompt_sequence: [ { prompt_audio_key: 'ivr/demo-language-ru', prompt_text: '\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430.', language: 'ru', }, { prompt_audio_key: 'ivr/demo-language-kz', prompt_text: '\u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437.', language: 'kz', }, ], is_terminal: false, invalid_target_node_id: 'root', no_input_target_node_id: 'root', options: [ { digit: '1', target_node_id: 'voice_start_kz' }, { digit: '2', target_node_id: 'voice_start_ru' }, ], }, { node_id: 'menu_ru', prompt_text: 'Для отдела продаж нажмите 1. Для службы поддержки нажмите 2.', prompt_audio_key: 'ivr/demo-menu-ru', is_terminal: false, invalid_target_node_id: 'menu_ru', no_input_target_node_id: 'menu_ru', options: [ { digit: '1', target_node_id: 'sales_ru' }, { digit: '2', target_node_id: 'support_ru' }, ], }, { node_id: 'menu_kz', prompt_text: 'Сату бөлімі үшін 1 басыңыз. Қолдау қызметі үшін 2 басыңыз.', prompt_audio_key: 'ivr/demo-menu-kz', is_terminal: false, invalid_target_node_id: 'menu_kz', no_input_target_node_id: 'menu_kz', options: [ { digit: '1', target_node_id: 'sales_kz' }, { digit: '2', target_node_id: 'support_kz' }, ], }, { node_id: 'sales_ru', prompt_text: 'Переводим в отдел продаж.', prompt_audio_key: 'ivr/demo-sales-ru', is_terminal: true, outcome_code: 'sales_route_ru', resolved_queue_id: 'sales_line', resolved_queue_code: 'ivr_sales_ai_ru', options: [], }, { node_id: 'support_ru', prompt_text: 'Переводим в службу поддержки.', prompt_audio_key: 'ivr/demo-support-ru', is_terminal: true, outcome_code: 'support_route_ru', resolved_queue_id: 'support_line', resolved_queue_code: 'ivr_support_ai_ru', options: [], }, { node_id: 'sales_kz', prompt_text: 'Сату бөліміне қосып жатырмыз.', prompt_audio_key: 'ivr/demo-sales-kz', is_terminal: true, outcome_code: 'sales_route_kz', resolved_queue_id: 'sales_line', resolved_queue_code: 'ivr_sales_ai_kz', options: [], }, { node_id: 'support_kz', prompt_text: 'Қолдау қызметіне қосып жатырмыз.', prompt_audio_key: 'ivr/demo-support-kz', is_terminal: true, outcome_code: 'support_route_kz', resolved_queue_id: 'support_line', resolved_queue_code: 'ivr_support_ai_kz', options: [], }, ], }; } function defaultVoiceStartIvrFlowDocument() { return { nodes: [ { node_id: 'root', prompt_text: '\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430. \u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437.', prompt_sequence: [ { prompt_audio_key: 'ivr/demo-language-ru', prompt_text: '\u0417\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435! \u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0446\u0435\u043d\u0442\u0440. \u0414\u043b\u044f \u0440\u0443\u0441\u0441\u043a\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0446\u0438\u0444\u0440\u0443 \u0434\u0432\u0430.', language: 'ru', }, { prompt_audio_key: 'ivr/demo-language-kz', prompt_text: '\u0421\u0430\u043b\u0430\u043c\u0430\u0442\u0441\u044b\u0437 \u0431\u0430! \u049a\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d \u0442\u0430\u04a3\u0434\u0430\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u0446\u0438\u0444\u0440\u044b\u043d \u0442\u0435\u0440\u0456\u04a3\u0456\u0437.', language: 'kz', }, ], is_terminal: false, invalid_target_node_id: 'root', no_input_target_node_id: 'root', options: [ { digit: '1', target_node_id: 'voice_start_kz' }, { digit: '2', target_node_id: 'voice_start_ru' }, ], }, { node_id: 'voice_start_kz', prompt_text: '\u049a\u043e\u04a3\u044b\u0440\u0430\u0443\u0434\u044b \u049b\u0430\u0437\u0430\u049b \u0442\u0456\u043b\u0456\u043d\u0434\u0435\u0433\u0456 \u0431\u0430\u0441\u0442\u0430\u043f\u049b\u044b voice-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439\u0433\u0435 \u04e9\u0442\u043a\u0456\u0437\u0456\u043f \u0436\u0430\u0442\u044b\u0440\u043c\u044b\u0437.', is_terminal: true, outcome_code: 'voice_start_kz', resolved_queue_id: 'voice_start_kz_line', resolved_queue_code: 'voice_start_kz', options: [], }, { node_id: 'voice_start_ru', prompt_text: '\u041f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u0437\u0432\u043e\u043d\u043e\u043a \u0432 \u0440\u0443\u0441\u0441\u043a\u043e\u044f\u0437\u044b\u0447\u043d\u044b\u0439 \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u044b\u0439 voice-\u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439.', is_terminal: true, outcome_code: 'voice_start_ru', resolved_queue_id: 'voice_start_ru_line', resolved_queue_code: 'voice_start_ru', options: [], }, ], }; } function ensureDefaultIvrFlowJson() { if (!$('ivrFlowJson').value.trim()) { $('ivrFlowJson').value = JSON.stringify(defaultVoiceStartIvrFlowDocument(), null, 2); } } function renderUsers(items) { if (!items.length) { $('usersList').innerHTML = '
  • Пользователи пока не найдены.
  • '; return; } $('usersList').innerHTML = items .map((item) => `
  • ${item.username}
    ${item.full_name}
    роль: ${item.role}
    ID: ${item.user_id}
  • `) .join(''); } function syncUserEditFields(user) { if (!user) { return; } $('userLookupId').value = user.user_id || $('userLookupId').value; $('editUserFullName').value = user.full_name || $('editUserFullName').value; $('editUserRole').value = user.role || $('editUserRole').value; } function renderQueues(items) { if (!items.length) { $('queuesList').innerHTML = '
  • Очереди пока не найдены.
  • '; $('routeQueueId').value = ''; $('updateQueueId').value = ''; $('ivrQueueId').value = ''; $('ivrTestQueueId').value = ''; return; } $('queuesList').innerHTML = items .map((item) => `
  • ${item.name}
    ID: ${item.queue_id}
    правил: ${(item.rules || []).length}
  • `) .join(''); } function renderIvrFlows(items) { if (!items.length) { $('ivrFlowsList').innerHTML = '
  • IVR-сценарии пока не настроены.
  • '; return; } $('ivrFlowsList').innerHTML = items .map( (item) => `
  • ${item.name}
    ID: ${item.flow_id}
    очередь: ${item.queue_id}
    активен: ${item.is_active}
  • `, ) .join(''); } function renderAsteriskEvents(items) { if (!items.length) { $('asteriskEventsList').innerHTML = '
  • Bridge-события Asterisk не найдены.
  • '; return; } $('asteriskEventsList').innerHTML = items .map( (item) => `
  • ${item.ami_event_name}
    событие: ${item.bridge_event_id}
    звонок: ${item.call_id}
    статус: ${item.forward_status}
  • `, ) .join(''); } function syncAsteriskEventFields(item) { if (!item) { return; } $('asteriskBridgeEventId').value = item.bridge_event_id || $('asteriskBridgeEventId').value; } function syncIvrFlowFields(flow) { if (!flow) { return; } $('ivrFlowId').value = flow.flow_id || $('ivrFlowId').value; $('ivrName').value = flow.name || $('ivrName').value; $('ivrDescription').value = flow.description || $('ivrDescription').value; $('ivrQueueId').value = flow.queue_id || $('ivrQueueId').value; $('ivrEntryNodeId').value = flow.entry_node_id || $('ivrEntryNodeId').value; $('ivrIsActive').checked = Boolean(flow.is_active); $('ivrFlowJson').value = JSON.stringify(flow.flow_json || defaultVoiceStartIvrFlowDocument(), null, 2); } function syncIvrSessionFields(payload) { const session = payload?.session || payload; if (!session) { return; } $('ivrSessionId').value = session.session_id || $('ivrSessionId').value; $('ivrTestCallId').value = session.call_id || $('ivrTestCallId').value; $('ivrTestQueueId').value = session.queue_id || $('ivrTestQueueId').value; $('ivrTestInteractionId').value = session.interaction_id || $('ivrTestInteractionId').value; } function serializeVoiceNameConfigForm() { return { enabled: $('voiceNameEnabled').checked, start: { ask_name_on_start: $('voiceNameAskOnStart').checked, known_customer_behavior: $('voiceNameKnownCustomerBehavior').value, unknown_customer_behavior: $('voiceNameUnknownCustomerBehavior').value, }, downstream: { missing_name_behavior: $('voiceNameMissingNameBehavior').value, uncertain_name_behavior: $('voiceNameUncertainNameBehavior').value, finalize_on_explicit_name: $('voiceNameFinalizeOnExplicitName').checked, finalize_on_confirmation: $('voiceNameFinalizeOnConfirmation').checked, }, texts: { ru: { start_prompt: $('voiceNameRuStartPrompt').value.trim(), personalized_greeting_template: $('voiceNameRuPersonalizedGreeting').value.trim(), confirmation_greeting_template: $('voiceNameRuConfirmationGreeting').value.trim(), inline_followup_prompt: $('voiceNameRuInlineFollowup').value.trim(), }, kz: { start_prompt: $('voiceNameKzStartPrompt').value.trim(), personalized_greeting_template: $('voiceNameKzPersonalizedGreeting').value.trim(), confirmation_greeting_template: $('voiceNameKzConfirmationGreeting').value.trim(), inline_followup_prompt: $('voiceNameKzInlineFollowup').value.trim(), }, }, }; } function syncVoiceNameConfigFields(payload) { const config = payload?.config || payload; if (!config) { return; } $('voiceNameEnabled').checked = Boolean(config.enabled); $('voiceNameAskOnStart').checked = Boolean(config.start?.ask_name_on_start); $('voiceNameKnownCustomerBehavior').value = config.start?.known_customer_behavior || 'trust_and_handoff'; $('voiceNameUnknownCustomerBehavior').value = config.start?.unknown_customer_behavior || 'ask_on_start'; $('voiceNameMissingNameBehavior').value = config.downstream?.missing_name_behavior || 'ask_inline_once'; $('voiceNameUncertainNameBehavior').value = config.downstream?.uncertain_name_behavior || 'confirm_then_finalize'; $('voiceNameFinalizeOnExplicitName').checked = Boolean(config.downstream?.finalize_on_explicit_name); $('voiceNameFinalizeOnConfirmation').checked = Boolean(config.downstream?.finalize_on_confirmation); $('voiceNameRuStartPrompt').value = config.texts?.ru?.start_prompt || ''; $('voiceNameRuPersonalizedGreeting').value = config.texts?.ru?.personalized_greeting_template || ''; $('voiceNameRuConfirmationGreeting').value = config.texts?.ru?.confirmation_greeting_template || ''; $('voiceNameRuInlineFollowup').value = config.texts?.ru?.inline_followup_prompt || ''; $('voiceNameKzStartPrompt').value = config.texts?.kz?.start_prompt || ''; $('voiceNameKzPersonalizedGreeting').value = config.texts?.kz?.personalized_greeting_template || ''; $('voiceNameKzConfirmationGreeting').value = config.texts?.kz?.confirmation_greeting_template || ''; $('voiceNameKzInlineFollowup').value = config.texts?.kz?.inline_followup_prompt || ''; } function renderVoiceNameConfigSummary(payload) { const config = payload?.config || payload; const source = payload?.source || 'defaults'; const updatedAt = payload?.updated_at || 'не сохранялось'; const knownLabels = { trust_and_handoff: 'известное имя сразу принимается и передаётся дальше', confirm_in_downstream: 'известное имя уходит на подтверждение в downstream', ask_on_start: 'известного клиента всё равно спрашиваем на старте', }; const unknownLabels = { ask_on_start: 'неизвестного клиента спрашиваем на старте', skip_to_downstream: 'неизвестного клиента сразу передаём в downstream', }; const missingLabels = { ask_inline_once: 'если имя не получено, AI спросит inline один раз', do_not_ask: 'если имя не получено, AI inline не спрашивает', }; const uncertainLabels = { confirm_then_finalize: 'неуверенное имя нужно подтвердить', finalize_immediately: 'неуверенное имя считается финальным сразу', discard_and_collect: 'неуверенное имя сбрасывается и собирается заново', }; $('voiceNameSummary').textContent = [ `Сценарий: ${config?.enabled ? 'включён' : 'выключен'}`, `Стартовый вопрос про имя: ${config?.start?.ask_name_on_start ? 'да' : 'нет'}`, `Известный клиент: ${knownLabels[config?.start?.known_customer_behavior] || config?.start?.known_customer_behavior || 'не задано'}`, `Неизвестный клиент: ${unknownLabels[config?.start?.unknown_customer_behavior] || config?.start?.unknown_customer_behavior || 'не задано'}`, `Если имя не получено: ${missingLabels[config?.downstream?.missing_name_behavior] || config?.downstream?.missing_name_behavior || 'не задано'}`, `Если имя неуверенное: ${uncertainLabels[config?.downstream?.uncertain_name_behavior] || config?.downstream?.uncertain_name_behavior || 'не задано'}`, `Финализация по явному имени: ${config?.downstream?.finalize_on_explicit_name ? 'да' : 'нет'}`, `Финализация по подтверждению: ${config?.downstream?.finalize_on_confirmation ? 'да' : 'нет'}`, `Источник конфигурации: ${source}`, `Последнее обновление: ${updatedAt}`, ].join('\n'); } async function loadVoiceNameConfig() { try { const data = await api('ai', 'ai/voice/config/name-collection'); state.voiceNameConfig = data; syncVoiceNameConfigFields(data); renderVoiceNameConfigSummary(data); $('voiceNameConfigOutput').textContent = JSON.stringify(data, null, 2); log('Настройки voice name collection загружены', { source: data.source, updated_at: data.updated_at, }); } catch (err) { $('voiceNameConfigOutput').textContent = err.message; log('Не удалось загрузить настройки voice name collection', { error: err.message }); } } async function saveVoiceNameConfig() { try { const payload = serializeVoiceNameConfigForm(); const data = await api('ai', 'ai/voice/config/name-collection', { method: 'PUT', body: JSON.stringify(payload), }); state.voiceNameConfig = data; syncVoiceNameConfigFields(data); renderVoiceNameConfigSummary(data); $('voiceNameConfigOutput').textContent = JSON.stringify(data, null, 2); log('Настройки voice name collection сохранены', { source: data.source, updated_at: data.updated_at, }); } catch (err) { $('voiceNameConfigOutput').textContent = err.message; log('Не удалось сохранить настройки voice name collection', { error: err.message }); } } function resetVoiceNameConfigForm() { const snapshot = state.voiceNameConfig; if (!snapshot) { $('voiceNameConfigOutput').textContent = 'Сначала загрузите текущие настройки voice name collection.'; return; } syncVoiceNameConfigFields(snapshot); renderVoiceNameConfigSummary(snapshot); $('voiceNameConfigOutput').textContent = JSON.stringify(snapshot, null, 2); log('Форма voice name collection сброшена к текущим настройкам'); } function voiceTtsProviderConfig(config, provider) { return config?.[provider] || { ru: {}, kz: {} }; } function voiceTtsOptionsFor(provider, language) { return state.voiceTtsConfig?.voice_options?.[provider]?.[language] || []; } function fillVoiceTtsPresetSelect(selectId, options, selectedValue) { const select = $(selectId); if (!select) { return; } const rows = [''].concat( (options || []).map((item) => ``), ); select.innerHTML = rows.join(''); const hasSelectedValue = (options || []).some((item) => item.value === selectedValue); select.value = hasSelectedValue ? selectedValue : ''; } function syncVoiceTtsPresetOptions(payload) { const config = payload?.config || payload || state.voiceTtsConfig?.config; if (!config) { return; } fillVoiceTtsPresetSelect('voiceTtsYandexRuVoicePreset', voiceTtsOptionsFor('yandex', 'ru'), config.yandex?.ru?.voice || ''); fillVoiceTtsPresetSelect('voiceTtsYandexKzVoicePreset', voiceTtsOptionsFor('yandex', 'kz'), config.yandex?.kz?.voice || ''); fillVoiceTtsPresetSelect('voiceTtsElevenRuVoicePreset', voiceTtsOptionsFor('elevenlabs', 'ru'), config.elevenlabs?.ru?.voice || ''); fillVoiceTtsPresetSelect('voiceTtsElevenKzVoicePreset', voiceTtsOptionsFor('elevenlabs', 'kz'), config.elevenlabs?.kz?.voice || ''); fillVoiceTtsPresetSelect('voiceTtsOpenAiRuVoicePreset', voiceTtsOptionsFor('openai', 'ru'), config.openai?.ru?.voice || ''); fillVoiceTtsPresetSelect('voiceTtsOpenAiKzVoicePreset', voiceTtsOptionsFor('openai', 'kz'), config.openai?.kz?.voice || ''); } function serializeVoiceTtsConfigForm() { return { provider: $('voiceTtsProvider').value || 'yandex', yandex: { ru: { voice: $('voiceTtsYandexRuVoice').value.trim() || null }, kz: { voice: $('voiceTtsYandexKzVoice').value.trim() || null }, }, elevenlabs: { ru: { voice: $('voiceTtsElevenRuVoice').value.trim() || null, model_id: $('voiceTtsElevenRuModel').value.trim() || null, language_code: $('voiceTtsElevenRuLanguageCode').value.trim() || null, }, kz: { voice: $('voiceTtsElevenKzVoice').value.trim() || null, model_id: $('voiceTtsElevenKzModel').value.trim() || null, language_code: $('voiceTtsElevenKzLanguageCode').value.trim() || null, }, }, openai: { ru: { voice: $('voiceTtsOpenAiRuVoice').value.trim() || null, model_id: $('voiceTtsOpenAiRuModel').value.trim() || null, language_code: $('voiceTtsOpenAiRuLanguageCode').value.trim() || null, }, kz: { voice: $('voiceTtsOpenAiKzVoice').value.trim() || null, model_id: $('voiceTtsOpenAiKzModel').value.trim() || null, language_code: $('voiceTtsOpenAiKzLanguageCode').value.trim() || null, }, }, }; } function syncVoiceTtsConfigFields(payload) { const config = payload?.config || payload; if (!config) { return; } $('voiceTtsProvider').value = config.provider || 'yandex'; $('voiceTtsYandexRuVoice').value = config.yandex?.ru?.voice || ''; $('voiceTtsYandexKzVoice').value = config.yandex?.kz?.voice || ''; $('voiceTtsElevenRuVoice').value = config.elevenlabs?.ru?.voice || ''; $('voiceTtsElevenRuModel').value = config.elevenlabs?.ru?.model_id || ''; $('voiceTtsElevenRuLanguageCode').value = config.elevenlabs?.ru?.language_code || ''; $('voiceTtsElevenKzVoice').value = config.elevenlabs?.kz?.voice || ''; $('voiceTtsElevenKzModel').value = config.elevenlabs?.kz?.model_id || ''; $('voiceTtsElevenKzLanguageCode').value = config.elevenlabs?.kz?.language_code || ''; $('voiceTtsOpenAiRuVoice').value = config.openai?.ru?.voice || ''; $('voiceTtsOpenAiRuModel').value = config.openai?.ru?.model_id || ''; $('voiceTtsOpenAiRuLanguageCode').value = config.openai?.ru?.language_code || ''; $('voiceTtsOpenAiKzVoice').value = config.openai?.kz?.voice || ''; $('voiceTtsOpenAiKzModel').value = config.openai?.kz?.model_id || ''; $('voiceTtsOpenAiKzLanguageCode').value = config.openai?.kz?.language_code || ''; syncVoiceTtsPresetOptions(payload); } function renderVoiceTtsConfigSummary(payload) { const config = payload?.config || payload; const source = payload?.source || 'defaults'; const updatedAt = payload?.updated_at || 'не сохранялось'; const activeProvider = config?.provider || 'yandex'; const activeSettings = voiceTtsProviderConfig(config, activeProvider); $('voiceTtsSummary').textContent = [ `Активный провайдер: ${activeProvider}`, `RU голос: ${activeSettings?.ru?.voice || 'не задан'}`, `RU модель: ${activeSettings?.ru?.model_id || 'по умолчанию'}`, `RU language_code: ${activeSettings?.ru?.language_code || 'по умолчанию'}`, `KZ голос: ${activeSettings?.kz?.voice || 'не задан'}`, `KZ модель: ${activeSettings?.kz?.model_id || 'по умолчанию'}`, `KZ language_code: ${activeSettings?.kz?.language_code || 'по умолчанию'}`, `Источник конфигурации: ${source}`, `Последнее обновление: ${updatedAt}`, ].join('\n'); } async function loadVoiceTtsConfig() { try { const data = await api('ai', 'ai/voice/config/tts'); state.voiceTtsConfig = data; syncVoiceTtsConfigFields(data); renderVoiceTtsConfigSummary(data); $('voiceTtsConfigOutput').textContent = JSON.stringify(data, null, 2); log('Настройки voice TTS загружены', { source: data.source, updated_at: data.updated_at }); } catch (err) { $('voiceTtsConfigOutput').textContent = err.message; log('Не удалось загрузить настройки voice TTS', { error: err.message }); } } async function saveVoiceTtsConfig() { try { const payload = serializeVoiceTtsConfigForm(); const data = await api('ai', 'ai/voice/config/tts', { method: 'PUT', body: JSON.stringify(payload), }); state.voiceTtsConfig = data; syncVoiceTtsConfigFields(data); renderVoiceTtsConfigSummary(data); $('voiceTtsConfigOutput').textContent = JSON.stringify(data, null, 2); log('Настройки voice TTS сохранены', { provider: data?.config?.provider, updated_at: data.updated_at }); } catch (err) { $('voiceTtsConfigOutput').textContent = err.message; log('Не удалось сохранить настройки voice TTS', { error: err.message }); } } function resetVoiceTtsConfigForm() { const snapshot = state.voiceTtsConfig; if (!snapshot) { $('voiceTtsConfigOutput').textContent = 'Сначала загрузите текущие настройки voice TTS.'; return; } syncVoiceTtsConfigFields(snapshot); renderVoiceTtsConfigSummary(snapshot); $('voiceTtsConfigOutput').textContent = JSON.stringify(snapshot, null, 2); log('Форма voice TTS сброшена к текущим настройкам'); } function bindVoiceTtsPreset(selectId, inputId) { const select = $(selectId); if (!select) { return; } select.addEventListener('change', () => { if (select.value) { $(inputId).value = select.value; } }); } function parseIvrFlowJson() { ensureDefaultIvrFlowJson(); return JSON.parse($('ivrFlowJson').value || '{}'); } async function loadUsers() { try { const data = await api('auth', 'users'); renderUsers(data); if (data.length && !$('userLookupId').value.trim()) { $('userLookupId').value = data[0].user_id; } log('Список пользователей обновлён', { count: data.length }); } catch (err) { $('usersList').innerHTML = '
  • Не удалось загрузить пользователей.
  • '; log('Не удалось загрузить пользователей', { error: err.message }); } } async function loadUserDetail() { const userId = $('userLookupId').value.trim(); if (!userId) { $('userDetailOutput').textContent = 'Укажите user_id для загрузки карточки.'; return; } try { const data = await api('auth', `users/${encodeURIComponent(userId)}`); $('userDetailOutput').textContent = JSON.stringify(data, null, 2); syncUserEditFields(data); log('Карточка пользователя загружена', { user_id: data.user_id, role: data.role }); } catch (err) { $('userDetailOutput').textContent = err.message; log('Не удалось загрузить карточку пользователя', { error: err.message }); } } async function createUser() { try { const data = await api('auth', 'users', { method: 'POST', body: JSON.stringify({ username: $('newUsername').value.trim() || 'wave2_admin_demo', password: $('newPassword').value, full_name: $('newFullName').value.trim() || 'Wave 2 Demo User', role: $('newUserRole').value, }), }); $('userDetailOutput').textContent = JSON.stringify(data, null, 2); syncUserEditFields(data); $('editUserPassword').value = ''; log('Пользователь создан', { username: data.username, role: data.role }); await loadUsers(); } catch (err) { $('userDetailOutput').textContent = err.message; log('Не удалось создать пользователя', { error: err.message }); } } async function updateUser() { const userId = $('userLookupId').value.trim(); if (!userId) { $('userDetailOutput').textContent = 'Укажите user_id для обновления пользователя.'; return; } const payload = { full_name: $('editUserFullName').value.trim() || null, role: $('editUserRole').value || null, }; const password = $('editUserPassword').value; if (password) { payload.password = password; } try { const data = await api('auth', `users/${encodeURIComponent(userId)}`, { method: 'PATCH', body: JSON.stringify(payload), }); $('userDetailOutput').textContent = JSON.stringify(data, null, 2); syncUserEditFields(data); $('editUserPassword').value = ''; log('Пользователь обновлён', { user_id: data.user_id, role: data.role }); await loadUsers(); } catch (err) { $('userDetailOutput').textContent = err.message; log('Не удалось обновить пользователя', { error: err.message }); } } async function deleteUser() { const userId = $('userLookupId').value.trim(); if (!userId) { $('userDetailOutput').textContent = 'Укажите user_id для удаления пользователя.'; return; } try { const data = await api('auth', `users/${encodeURIComponent(userId)}`, { method: 'DELETE', }); $('userDetailOutput').textContent = JSON.stringify(data, null, 2); $('userLookupId').value = ''; $('editUserPassword').value = ''; log('Пользователь удалён', { user_id: data.user_id }); await loadUsers(); } catch (err) { $('userDetailOutput').textContent = err.message; log('Не удалось удалить пользователя', { error: err.message }); } } async function loadQueues() { try { const data = await api('routing', 'queues'); renderQueues(data); if (data.length) { const first = data[0]; if (!$('routeQueueId').value.trim()) { $('routeQueueId').value = first.queue_id; } if (!$('updateQueueId').value.trim()) { $('updateQueueId').value = first.queue_id; } if (!$('ivrQueueId').value.trim()) { $('ivrQueueId').value = first.queue_id; } if (!$('ivrTestQueueId').value.trim()) { $('ivrTestQueueId').value = first.queue_id; } } log('Список очередей обновлён', { count: data.length }); } catch (err) { $('queuesList').innerHTML = '
  • Не удалось загрузить очереди.
  • '; log('Не удалось загрузить очереди', { error: err.message }); } } async function createQueue() { try { const payload = { name: $('queueName').value.trim() || 'Демонстрационная очередь', description: $('queueDescription').value.trim() || '', rules: queueRulePayload($('queueChannel').value, $('queueSla').value), }; const data = await api('routing', 'queues', { method: 'POST', body: JSON.stringify(payload), }); $('routeQueueId').value = data.queue_id; $('updateQueueId').value = data.queue_id; if (!$('ivrQueueId').value.trim()) { $('ivrQueueId').value = data.queue_id; } if (!$('ivrTestQueueId').value.trim()) { $('ivrTestQueueId').value = data.queue_id; } $('queueUpdateOutput').textContent = JSON.stringify(data, null, 2); log('Очередь создана', { queue_id: data.queue_id, channel: $('queueChannel').value }); await loadQueues(); } catch (err) { $('queueUpdateOutput').textContent = err.message; log('Не удалось создать очередь', { error: err.message }); } } async function updateQueueRules() { const queueId = $('updateQueueId').value.trim(); if (!queueId) { $('queueUpdateOutput').textContent = 'Укажите queue_id для обновления.'; return; } try { const payload = { name: $('updateQueueName').value.trim() || 'Демонстрационная очередь обновлена', description: $('updateQueueDescription').value.trim() || '', rules: queueRulePayload($('updateQueueChannel').value, $('updateQueueSla').value), }; const data = await api('routing', `queues/${encodeURIComponent(queueId)}/rules`, { method: 'PATCH', body: JSON.stringify(payload), }); $('queueUpdateOutput').textContent = JSON.stringify(data, null, 2); $('routeQueueId').value = data.queue_id; log('Правила очереди обновлены', { queue_id: data.queue_id, rules: (data.rules || []).length }); await loadQueues(); } catch (err) { $('queueUpdateOutput').textContent = err.message; log('Не удалось обновить очередь', { error: err.message }); } } async function deleteQueue() { const queueId = $('updateQueueId').value.trim(); if (!queueId) { $('queueUpdateOutput').textContent = 'Укажите queue_id для удаления очереди.'; return; } try { const data = await api('routing', `queues/${encodeURIComponent(queueId)}`, { method: 'DELETE', }); $('queueUpdateOutput').textContent = JSON.stringify(data, null, 2); if ($('routeQueueId').value.trim() === queueId) { $('routeQueueId').value = ''; } if ($('ivrQueueId').value.trim() === queueId) { $('ivrQueueId').value = ''; } if ($('ivrTestQueueId').value.trim() === queueId) { $('ivrTestQueueId').value = ''; } $('updateQueueId').value = ''; log('Очередь удалена', { queue_id: data.queue_id }); await loadQueues(); } catch (err) { $('queueUpdateOutput').textContent = err.message; log('Не удалось удалить очередь', { error: err.message }); } } async function previewRoute() { try { const queueId = encodeURIComponent($('routeQueueId').value.trim()); const channel = encodeURIComponent($('routeChannel').value); const priority = Number($('routePriority').value || 3); const data = await api('routing', `queues/${queueId}/route?channel=${channel}&priority=${priority}`, { method: 'POST', }); $('routeOutput').textContent = JSON.stringify(data, null, 2); log('Маршрут рассчитан', { queue_id: data.queue_id, assignee: data.assignee }); } catch (err) { $('routeOutput').textContent = err.message; log('Не удалось рассчитать маршрут', { error: err.message }); } } async function loadIvrFlows() { try { const data = await api('ivr', 'ivr/flows'); renderIvrFlows(data); if (data.length && !$('ivrFlowId').value.trim()) { syncIvrFlowFields(data[0]); } log('IVR-сценарии загружены', { count: data.length }); } catch (err) { $('ivrFlowsList').innerHTML = '
  • Не удалось загрузить IVR-сценарии.
  • '; $('ivrFlowOutput').textContent = err.message; log('Не удалось загрузить IVR-сценарии', { error: err.message }); } } async function loadIvrFlowDetail() { const flowId = $('ivrFlowId').value.trim(); if (!flowId) { $('ivrFlowOutput').textContent = 'Укажите ID сценария для открытия IVR.'; return; } try { const data = await api('ivr', `ivr/flows/${encodeURIComponent(flowId)}`); syncIvrFlowFields(data); $('ivrFlowOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сценарий загружен', { flow_id: data.flow_id, active: data.is_active }); } catch (err) { $('ivrFlowOutput').textContent = err.message; log('Не удалось загрузить IVR-сценарий', { error: err.message }); } } async function createIvrFlow() { try { const data = await api('ivr', 'ivr/flows', { method: 'POST', body: JSON.stringify({ name: $('ivrName').value.trim() || 'Демонстрационный IVR-сценарий', description: $('ivrDescription').value.trim() || '', queue_id: $('ivrQueueId').value.trim(), entry_node_id: $('ivrEntryNodeId').value.trim() || 'root', flow_json: parseIvrFlowJson(), is_active: $('ivrIsActive').checked, }), }); syncIvrFlowFields(data); $('ivrFlowOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сценарий создан', { flow_id: data.flow_id, queue_id: data.queue_id }); await loadIvrFlows(); } catch (err) { $('ivrFlowOutput').textContent = err.message; log('Не удалось создать IVR-сценарий', { error: err.message }); } } async function updateIvrFlow() { const flowId = $('ivrFlowId').value.trim(); if (!flowId) { $('ivrFlowOutput').textContent = 'Укажите ID сценария для обновления IVR.'; return; } try { const data = await api('ivr', `ivr/flows/${encodeURIComponent(flowId)}`, { method: 'PATCH', body: JSON.stringify({ name: $('ivrName').value.trim() || null, description: $('ivrDescription').value.trim() || null, entry_node_id: $('ivrEntryNodeId').value.trim() || null, flow_json: parseIvrFlowJson(), is_active: $('ivrIsActive').checked, }), }); syncIvrFlowFields(data); $('ivrFlowOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сценарий обновлён', { flow_id: data.flow_id, version: data.version }); await loadIvrFlows(); } catch (err) { $('ivrFlowOutput').textContent = err.message; log('Не удалось обновить IVR-сценарий', { error: err.message }); } } async function activateIvrFlow() { const flowId = $('ivrFlowId').value.trim(); if (!flowId) { $('ivrFlowOutput').textContent = 'Укажите ID сценария для активации IVR.'; return; } try { const data = await api('ivr', `ivr/flows/${encodeURIComponent(flowId)}/activate`, { method: 'POST', }); syncIvrFlowFields(data); $('ivrFlowOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сценарий активирован', { flow_id: data.flow_id }); await loadIvrFlows(); } catch (err) { $('ivrFlowOutput').textContent = err.message; log('Не удалось активировать IVR-сценарий', { error: err.message }); } } async function startIvrSession() { try { const data = await api('ivr', 'ivr/sessions/start', { method: 'POST', body: JSON.stringify({ call_id: $('ivrTestCallId').value.trim() || 'demo_call_ivr', queue_id: $('ivrTestQueueId').value.trim(), interaction_id: $('ivrTestInteractionId').value.trim() || null, }), }); syncIvrSessionFields(data); $('ivrSessionOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сессия запущена', { session_id: data.session.session_id, flow_id: data.session.flow_id }); } catch (err) { $('ivrSessionOutput').textContent = err.message; log('Не удалось запустить IVR-сессию', { error: err.message }); } } async function sendIvrDigit() { const sessionId = $('ivrSessionId').value.trim(); if (!sessionId) { $('ivrSessionOutput').textContent = 'Укажите ID сессии для отправки DTMF.'; return; } try { const data = await api('ivr', `ivr/sessions/${encodeURIComponent(sessionId)}/dtmf`, { method: 'POST', body: JSON.stringify({ digit: ($('ivrDigit').value.trim() || '2').slice(0, 1), }), }); syncIvrSessionFields(data); $('ivrSessionOutput').textContent = JSON.stringify(data, null, 2); log('DTMF обработан', { session_id: data.session.session_id, completed: data.completed, outcome_code: data.session.outcome_code, }); } catch (err) { $('ivrSessionOutput').textContent = err.message; log('Не удалось обработать DTMF', { error: err.message }); } } async function loadIvrSession() { const sessionId = $('ivrSessionId').value.trim(); if (!sessionId) { $('ivrSessionOutput').textContent = 'Укажите ID сессии для загрузки IVR.'; return; } try { const data = await api('ivr', `ivr/sessions/${encodeURIComponent(sessionId)}`); syncIvrSessionFields(data); $('ivrSessionOutput').textContent = JSON.stringify(data, null, 2); log('IVR-сессия загружена', { session_id: data.session_id, status: data.status }); } catch (err) { $('ivrSessionOutput').textContent = err.message; log('Не удалось загрузить IVR-сессию', { error: err.message }); } } async function previewIvrRoute() { const queueId = $('ivrTestQueueId').value.trim(); const sessionId = $('ivrSessionId').value.trim(); if (!queueId || !sessionId) { $('ivrRouteOutput').textContent = 'Укажите ID очереди и ID сессии для предпросмотра IVR-маршрута.'; return; } try { const data = await api( 'routing', `queues/${encodeURIComponent(queueId)}/route?channel=voice&priority=3&ivr_session_id=${encodeURIComponent(sessionId)}`, { method: 'POST', }, ); $('ivrRouteOutput').textContent = JSON.stringify(data, null, 2); log('Предпросмотр IVR-маршрута готов', { original_queue_id: data.original_queue_id || queueId, resolved_queue_id: data.resolved_queue_id || data.queue_id, }); } catch (err) { $('ivrRouteOutput').textContent = err.message; log('Не удалось показать IVR-маршрут', { error: err.message }); } } async function loadAsteriskStatus() { try { const data = await api('asterisk-bridge', 'asterisk/status'); $('asteriskStatusOutput').textContent = JSON.stringify(data, null, 2); log('Статус моста Asterisk загружен', { status: data.status, ami_connected: data.ami_connected, }); } catch (err) { $('asteriskStatusOutput').textContent = err.message; log('Не удалось загрузить статус моста Asterisk', { error: err.message }); } } async function loadAsteriskEvents() { try { const filter = $('asteriskEventStatus').value; const suffix = filter ? `?status=${encodeURIComponent(filter)}` : ''; const data = await api('asterisk-bridge', `asterisk/events${suffix}`); renderAsteriskEvents(data); if (data.length && !$('asteriskBridgeEventId').value.trim()) { syncAsteriskEventFields(data[0]); } log('Bridge-события Asterisk загружены', { count: data.length, status: filter || 'all' }); } catch (err) { $('asteriskEventsList').innerHTML = '
  • Не удалось загрузить bridge-события Asterisk.
  • '; $('asteriskEventOutput').textContent = err.message; log('Не удалось загрузить bridge-события Asterisk', { error: err.message }); } } async function loadAsteriskEventDetail() { const bridgeEventId = $('asteriskBridgeEventId').value.trim(); if (!bridgeEventId) { $('asteriskEventOutput').textContent = 'Укажите ID bridge-события для открытия.'; return; } try { const data = await api('asterisk-bridge', `asterisk/events/${encodeURIComponent(bridgeEventId)}`); syncAsteriskEventFields(data); $('asteriskEventOutput').textContent = JSON.stringify(data, null, 2); log('Bridge-событие Asterisk загружено', { bridge_event_id: data.bridge_event_id, status: data.forward_status, }); } catch (err) { $('asteriskEventOutput').textContent = err.message; log('Не удалось загрузить bridge-событие Asterisk', { error: err.message }); } } async function retryAsteriskEvent() { const bridgeEventId = $('asteriskBridgeEventId').value.trim(); if (!bridgeEventId) { $('asteriskEventOutput').textContent = 'Укажите ID bridge-события для повтора.'; return; } try { const data = await api('asterisk-bridge', `asterisk/events/${encodeURIComponent(bridgeEventId)}/retry`, { method: 'POST', }); syncAsteriskEventFields(data); $('asteriskEventOutput').textContent = JSON.stringify(data, null, 2); log('Bridge-событие Asterisk отправлено на повтор', { bridge_event_id: data.bridge_event_id, status: data.forward_status, }); await loadAsteriskEvents(); } catch (err) { $('asteriskEventOutput').textContent = err.message; log('Не удалось повторить bridge-событие Asterisk', { error: err.message }); } } async function reconnectAsterisk() { try { const data = await api('asterisk-bridge', 'asterisk/reconnect', { method: 'POST', }); $('asteriskStatusOutput').textContent = JSON.stringify(data, null, 2); log('Запрошено переподключение моста Asterisk', { status: data.status, ami_connected: data.ami_connected, }); } catch (err) { $('asteriskStatusOutput').textContent = err.message; log('Не удалось запросить переподключение Asterisk', { error: err.message }); } } function wire() { $('loginBtn').addEventListener('click', login); $('corporateLoginBtn').addEventListener('click', startCorporateLogin); $('refreshBtn').addEventListener('click', async () => { await Promise.all([ loadUsers(), loadQueues(), loadIvrFlows(), loadVoiceNameConfig(), loadVoiceTtsConfig(), loadAsteriskStatus(), loadAsteriskEvents(), ]); log('Данные админ-консоли обновлены'); }); $('loadUsersBtn').addEventListener('click', loadUsers); $('loadUserDetailBtn').addEventListener('click', loadUserDetail); $('createUserBtn').addEventListener('click', createUser); $('updateUserBtn').addEventListener('click', updateUser); $('deleteUserBtn').addEventListener('click', deleteUser); $('loadQueuesBtn').addEventListener('click', loadQueues); $('createQueueBtn').addEventListener('click', createQueue); $('updateQueueBtn').addEventListener('click', updateQueueRules); $('deleteQueueBtn').addEventListener('click', deleteQueue); $('previewRouteBtn').addEventListener('click', previewRoute); $('loadIvrFlowsBtn').addEventListener('click', loadIvrFlows); $('createIvrFlowBtn').addEventListener('click', createIvrFlow); $('loadIvrFlowDetailBtn').addEventListener('click', loadIvrFlowDetail); $('updateIvrFlowBtn').addEventListener('click', updateIvrFlow); $('activateIvrFlowBtn').addEventListener('click', activateIvrFlow); $('startIvrSessionBtn').addEventListener('click', startIvrSession); $('sendIvrDigitBtn').addEventListener('click', sendIvrDigit); $('loadIvrSessionBtn').addEventListener('click', loadIvrSession); $('previewIvrRouteBtn').addEventListener('click', previewIvrRoute); $('loadVoiceNameConfigBtn').addEventListener('click', loadVoiceNameConfig); $('saveVoiceNameConfigBtn').addEventListener('click', saveVoiceNameConfig); $('resetVoiceNameConfigBtn').addEventListener('click', resetVoiceNameConfigForm); $('loadVoiceTtsConfigBtn').addEventListener('click', loadVoiceTtsConfig); $('saveVoiceTtsConfigBtn').addEventListener('click', saveVoiceTtsConfig); $('resetVoiceTtsConfigBtn').addEventListener('click', resetVoiceTtsConfigForm); bindVoiceTtsPreset('voiceTtsYandexRuVoicePreset', 'voiceTtsYandexRuVoice'); bindVoiceTtsPreset('voiceTtsYandexKzVoicePreset', 'voiceTtsYandexKzVoice'); bindVoiceTtsPreset('voiceTtsElevenRuVoicePreset', 'voiceTtsElevenRuVoice'); bindVoiceTtsPreset('voiceTtsElevenKzVoicePreset', 'voiceTtsElevenKzVoice'); bindVoiceTtsPreset('voiceTtsOpenAiRuVoicePreset', 'voiceTtsOpenAiRuVoice'); bindVoiceTtsPreset('voiceTtsOpenAiKzVoicePreset', 'voiceTtsOpenAiKzVoice'); $('loadAsteriskStatusBtn').addEventListener('click', loadAsteriskStatus); $('loadAsteriskEventsBtn').addEventListener('click', loadAsteriskEvents); $('loadAsteriskEventDetailBtn').addEventListener('click', loadAsteriskEventDetail); $('retryAsteriskEventBtn').addEventListener('click', retryAsteriskEvent); $('reconnectAsteriskBtn').addEventListener('click', reconnectAsterisk); $('logoutBtn').addEventListener('click', logout); window.addEventListener('message', handleOidcMessage); } async function init() { if (!restoreStoredSession()) { window.location.href = '/'; return; } if (!ensurePageAccess()) { return; } wire(); ensureDefaultIvrFlowJson(); updateSessionInfo(); await Promise.all([ checkGateway(), loadOidcConfig(), loadUsers(), loadQueues(), loadIvrFlows(), loadVoiceNameConfig(), loadVoiceTtsConfig(), loadAsteriskStatus(), loadAsteriskEvents(), ]); log('Админ-консоль готова'); } init();