diff --git a/ui/admin/app.js b/ui/admin/app.js new file mode 100644 index 0000000..93f6a6d --- /dev/null +++ b/ui/admin/app.js @@ -0,0 +1,1418 @@ +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(); diff --git a/ui/admin/index.html b/ui/admin/index.html new file mode 100644 index 0000000..78affba --- /dev/null +++ b/ui/admin/index.html @@ -0,0 +1,506 @@ + + + + + + KonturCC Администрирование + + + + + + + +
    + + +
    +
    + +
    +
    Шлюз: проверка...
    + +
    +
    AD
    +
    +
    Экран администратора
    +
    Панель управления
    +
    +
    +
    +
    + +
    +
    +

    Административный контур

    +

    Панель администратора

    +

    Управление пользователями, очередями и предпросмотром маршрутизации без технического режима.

    +
    + +
    +

    Сессия

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + +
    +

    Токен: отсутствует

    +
    + +
    +
    +

    Пользователи

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    Карточка пользователя появится здесь.
    +
      +
      + +
      +

      Очереди

      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + +
      +
      Результат обновления очереди появится здесь.
      +
        +
        + +
        +

        Предпросмотр маршрута

        +
        + + +
        +
        + + +
        +
        Результат маршрутизации появится здесь.
        +
        + +
        +

        IVR

        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + + + + +
        +
          +
          Детали IVR-сценария появятся здесь.
          + +
          + + +
          +
          + + +
          +
          + + + + + +
          +
          Состояние IVR-сессии появится здесь.
          +
          Предпросмотр IVR-маршрута появится здесь.
          +
          + +
          +

          Voice AI: сбор имени

          +

          Глобальная админ-настройка сценария определения имени клиента для voice_start и downstream AI.

          +
          Сводка по текущим настройкам появится здесь.
          + +
          + + +
          + +
          + + +
          + +
          + + +
          + +
          + + +
          + +
          + + + + + +
          + +
          + + + + + +
          + +
          + + + +
          +
          Raw payload voice name collection появится здесь.
          +
          + +
          +

          Voice AI: TTS

          +

          Выберите провайдера и голоса для RU и KZ. Для ElevenLabs в поле голоса хранится voice ID, для Brian уже есть готовый preset.

          +
          Сводка по voice TTS появится здесь.
          + +
          + +
          RU и KZ можно настроить отдельно, а провайдер переключать из админки без правки env.
          +
          + +
          + +
          + + +
          +
          + + +
          +
          + +
          + +
          + + +
          +
          + + +
          +
          + + +
          +
          + + +
          +
          + +
          + +
          + + +
          +
          + + +
          +
          + + +
          +
          + + +
          +
          + +
          + + + +
          +
          Raw payload voice TTS появится здесь.
          +
          + +
          +

          Мост Asterisk

          +

          Диагностика AMI-подключения, пересланных bridge-событий и ручного повтора.

          +
          + + +
          +
          + + +
          +
          + + + +
          +
          Статус моста Asterisk появится здесь.
          +
            +
            Детали bridge-события Asterisk появятся здесь.
            +
            + +
            +

            Лог действий

            +

            Сверху всегда последнее действие администратора.

            +
            
            +          
            +
            +
            +
            +
            + + + + diff --git a/ui/analyst/app.js b/ui/analyst/app.js new file mode 100644 index 0000000..5a84ad0 --- /dev/null +++ b/ui/analyst/app.js @@ -0,0 +1,7927 @@ +const state = { + user: 'analyst', + role: 'analyst', + token: null, + authSource: 'local', + fullName: null, + analytics: { + preset: '7d', + fromTs: '', + toTs: '', + queueId: 'all', + channel: 'all', + compareMode: 'previous', + trendMetric: 'volume', + voiceNameTrendMetric: 'scenario_calls', + aiTrendMetric: 'containment_rate', + agentTrendMetric: 'interactions_per_agent', + overview: null, + compare: null, + trend: [], + voiceNameOverview: null, + voiceNameCompare: null, + voiceNameTrend: [], + aiOverview: null, + aiCompare: null, + aiTrend: [], + agentOverview: null, + agentCompare: null, + agentTrend: [], + channelRows: [], + queueRows: [], + agentRows: [], + coverage: null, + queueOptions: [], + savedViews: [], + activeViewId: '', + lastRangeMeta: null, + trendInterval: 'day', + agentTrendInterval: 'day', + statusMessage: '', + mockData: null, + mockDataError: '', + loading: false, + error: '', + voiceNameError: '', + voiceNameTrendError: '', + aiError: '', + agentError: '', + agentTrendError: '', + queueAccessError: '', + requestId: 0, + }, + drilldown: { + open: false, + mode: 'interaction', + sourceType: '', + sourceLabel: '', + sourceValue: '', + metric: '', + filters: null, + coverage: null, + metricNote: '', + items: [], + total: 0, + limit: 12, + offset: 0, + selectedInteractionId: '', + selectedInteraction: null, + selectedLinkedInteraction: null, + selectedTimeline: [], + loading: false, + detailLoading: false, + exporting: false, + notice: '', + error: '', + }, + oidc: { + enabled: false, + loginPath: '/auth/oidc/start?return_mode=popup', + providerLabel: 'Keycloak', + }, +}; + +const $ = (id) => document.getElementById(id); +const SESSION_STORAGE_KEY = 'cc_session'; +const DRILLDOWN_PAGE_SIZE = 12; +const DRILLDOWN_EXPORT_BATCH_SIZE = 100; +const DRILLDOWN_EXPORT_SOFT_CAP = 1000; +const ANALYTICS_TREND_OPTIONS = ['volume', 'SL', 'ASA', 'Abandon']; +const VOICE_NAME_TREND_OPTIONS = [ + 'scenario_calls', + 'start_capture_rate', + 'downstream_rescue_rate', + 'handoff_unconfirmed_rate', + 'manual_correction_rate', +]; +const AI_ANALYTICS_TREND_OPTIONS = [ + 'containment_rate', + 'handoff_rate', + 'human_touched_rate', + 'ai_latency_avg_ms', + 'closed_without_operator_rate', +]; +const AGENT_ANALYTICS_TREND_OPTIONS = [ + 'interactions_per_agent', + 'agents_with_activity', + 'avg_handle_seconds', + 'fcr_rate', +]; +const ENABLE_ANALYTICS_MOCK_DATA = true; +const ANALYTICS_MOCK_DATA_URL = '/analyst/assets/mock-analytics.json'; +const ANALYTICS_DEEP_LINK_KEYS = [ + 'preset', + 'from', + 'to', + 'queue', + 'channel', + 'compare', + 'trend', + 'voice_name_trend', + 'ai_trend', + 'agent_trend', + 'view', + 'dd', + 'dd_mode', + 'dd_source', + 'dd_value', + 'dd_metric', + 'dd_slice', + 'dd_reason', + 'dd_status', + 'dd_q', + 'dd_sort_by', + 'dd_sort_dir', + 'dd_offset', + 'dd_selected', + 'dd_agent', +]; +let analyticsDrilldownSearchDebounce = 0; +let analyticsDrilldownReturnFocus = null; +let analyticsUrlSyncSuspended = false; +const ROLE_LABELS = { + admin: 'Администратор', + supervisor: 'Супервизор', + operator: 'Оператор', + analyst: 'Аналитик', +}; + +const ANALYTICS_CHANNEL_OPTIONS = [ + { value: 'all', label: 'Все каналы' }, + { value: 'voice', label: 'Голос' }, + { value: 'telegram', label: 'Telegram' }, + { value: 'whatsapp', label: 'WhatsApp' }, + { value: 'webchat', label: 'Веб-чат' }, + { value: 'email', label: 'Email' }, +]; + +const ANALYTICS_PRESET_LABELS = { + today: 'Сегодня', + '7d': 'Последние 7 дней', + '30d': 'Последние 30 дней', +}; + +const ANALYTICS_COMPARE_MODE_LABELS = { + previous: 'С предыдущим окном', + off: 'Только текущий срез', +}; + +const ANALYTICS_TREND_LABELS = { + volume: 'Обращения', + SL: 'SLA', + ASA: 'Среднее ожидание', + Abandon: 'Потери', +}; + +const VOICE_NAME_TREND_LABELS = { + scenario_calls: 'Звонки в сценарии', + start_capture_rate: 'Имя взято сразу', + downstream_rescue_rate: 'Имя добрал AI после follow-up', + handoff_unconfirmed_rate: 'Передача без подтверждённого имени', + manual_correction_rate: 'Ручное исправление', +}; + +const VOICE_NAME_METRIC_META = { + scenario_calls: { unit: 'count', better: 'up', note: 'Все voice-звонки, прошедшие через сценарий сбора имени.' }, + start_capture_rate: { unit: 'pct', better: 'up', note: 'Доля звонков, где имя удалось взять сразу на стартовом этапе.' }, + downstream_rescue_rate: { unit: 'pct', better: 'up', note: 'Доля кейсов, где AI успешно добрал имя после стартового follow-up.' }, + handoff_unconfirmed_rate: { unit: 'pct', better: 'down', note: 'Доля звонков, переданных оператору без подтверждённого имени.' }, + manual_correction_rate: { unit: 'pct', better: 'down', note: 'Как часто оператору пришлось исправлять имя вручную.' }, +}; + +const VOICE_NAME_FUNNEL_LABELS = { + scenario_calls: 'Звонки в сценарии', + start_obtained: 'Имя взято сразу', + needed_downstream: 'Потребовался AI после старта', + downstream_ai_obtained: 'Имя добрал AI после follow-up', + handoff_confirmed_name: 'Передача с подтверждённым именем', + handoff_unconfirmed_name: 'Передача без подтверждённого имени', +}; + +const VOICE_NAME_HANDOFF_LABELS = { + confirmed_name: 'Передача с подтверждённым именем', + unconfirmed_name: 'Передача без подтверждённого имени', +}; + +const ANALYTICS_METRIC_META = { + total: { unit: 'count', better: 'up', note: 'Все обращения за выбранный период' }, + answered: { unit: 'count', better: 'up', note: 'Обращения, обработанные без потери' }, + SL: { unit: 'pct', better: 'up', note: 'Доля обращений, уложившихся в SLA' }, + ASA: { unit: 'seconds', better: 'down', note: 'Среднее ожидание до ответа' }, + AHT: { unit: 'seconds', better: 'down', note: 'Среднее время обработки обращения' }, + Abandon: { unit: 'pct', better: 'down', note: 'Доля потерянных обращений' }, + FCR: { unit: 'pct', better: 'up', note: 'Решено с первого контакта' }, + DigitalShare: { unit: 'pct', better: 'neutral', note: 'Доля цифровых каналов в потоке' }, + volume: { unit: 'count', better: 'up', note: 'Объём обращений в интервале' }, +}; + +const AI_ANALYTICS_TREND_LABELS = { + containment_rate: 'Закрыто AI', + handoff_rate: 'Передано оператору', + ai_latency_avg_ms: 'Задержка AI', + closed_without_operator_rate: 'Закрыто без оператора', + human_touched_rate: 'С участием оператора', +}; + +const AI_ANALYTICS_METRIC_META = { + containment_rate: { + unit: 'pct', + better: 'up', + note: 'Доля AI-сессий, закрытых самим AI без передачи оператору и без назначения исполнителя.', + }, + handoff_rate: { + unit: 'pct', + better: 'down', + note: 'Доля AI-сессий, которые завершились передачей оператору или ушли в ручное ведение.', + }, + ai_latency_avg_ms: { + unit: 'ms', + better: 'down', + note: 'Средняя задержка AI-ответа по ответам модели.', + }, + closed_without_operator_rate: { + unit: 'pct', + better: 'up', + note: 'Среди закрытых AI-связанных обращений доля кейсов без назначения оператора.', + }, + human_touched_rate: { + unit: 'pct', + better: 'down', + note: 'Доля сессий, где участвовал оператор: через назначение или ручной takeover.', + }, +}; + +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() { + const allowed = ['admin', 'supervisor', 'analyst']; + if (!allowed.includes(state.role)) { + 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 syncSessionInputs() { + const sessionUser = $('sessionUser'); + const sessionRole = $('sessionRole'); + if (sessionUser) { + sessionUser.value = state.user; + } + if (sessionRole) { + sessionRole.value = state.role; + } +} + +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; + syncSessionInputs(); + updateProfileMeta(); + applyRoleNavigation(); + return true; + } catch { + clearStoredSession(); + return false; + } +} + +function logout() { + clearStoredSession(); + window.location.href = '/'; +} + +function syncSessionFromInputs() { + const sessionUser = $('sessionUser'); + const sessionRole = $('sessionRole'); + state.user = sessionUser?.value.trim() || state.user; + state.role = sessionRole?.value || state.role; +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function renderSummaryCard(label, value, note = '') { + return ` +
            +
            ${label}
            +
            ${value}
            + ${note ? `
            ${note}
            ` : ''} +
            + `; +} + +function updateProfileMeta() { + const profileName = $('profileName'); + const profileRole = $('profileRole'); + if (!profileName || !profileRole) { + return; + } + profileName.textContent = state.fullName || state.user || 'Пользователь KonturCC'; + profileRole.textContent = ROLE_LABELS[state.role] || state.role || 'Пользователь'; +} + +function updateSessionInfo(extra = '') { + const box = $('sessionInfo'); + if (!box) { + return; + } + if (!state.token) { + box.textContent = 'Токен: отсутствует'; + return; + } + const source = state.authSource === 'oidc' ? 'корпоративный' : 'локальный'; + box.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; + syncSessionInputs(); + updateSessionInfo(data.provider ? `провайдер: ${data.provider}` : ''); + updateProfileMeta(); + persistSession(); + applyRoleNavigation(); + ensurePageAccess(); +} + +function buildProxyHeaders(extra = {}) { + syncSessionFromInputs(); + const headers = { + 'X-User': state.user, + 'X-Role': state.role, + ...extra, + }; + if (state.token) { + headers.Authorization = `Bearer ${state.token}`; + } + return headers; +} + +async function api(service, path, options = {}) { + const headers = buildProxyHeaders({ + 'Content-Type': 'application/json', + ...(options.headers || {}), + }); + + 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; +} + +function contentDispositionFilename(value) { + if (!value) { + return ''; + } + const match = /filename=\"?([^\";]+)\"?/i.exec(value); + return match ? decodeURIComponent(match[1]) : ''; +} + +async function downloadProxyFile(service, path, fallbackFilename) { + const response = await fetch(`/proxy/${service}/${path}`, { + headers: buildProxyHeaders(), + }); + if (!response.ok) { + let detail = ''; + try { + detail = JSON.stringify(await response.json()); + } catch { + detail = await response.text(); + } + throw new Error(`${response.status}: ${detail}`); + } + const blob = await response.blob(); + const filename = contentDispositionFilename(response.headers.get('content-disposition')) || fallbackFilename; + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + return filename; +} + +async function checkGateway() { + const pill = $('gatewayStatus'); + if (!pill) { + return; + } + try { + const response = await fetch('/health'); + const data = await response.json(); + pill.textContent = `Данные: ${data.status === 'ok' ? 'готовы' : data.status}`; + } catch { + pill.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'; + } catch { + state.oidc.enabled = false; + } +} + +function handleOidcMessage(event) { + if (!event?.data || typeof event.data !== 'object') { + return; + } + if (event.data.type === 'oidc-login' && event.data.access_token) { + applyAuthenticatedSession(event.data); + return; + } + if (event.data.type === 'oidc-error') { + updateSessionInfo(`ошибка SSO: ${event.data.message || 'неизвестно'}`); + } +} + +function emptyKpiEnvelope(fromTs = null, toTs = null) { + return { + window: { from: fromTs, to: toTs }, + volume: { + total: 0, + answered: 0, + abandoned: 0, + }, + kpi: { + SL: 0, + ASA: 0, + AHT: 0, + Abandon: 0, + FCR: 0, + AnswerRate: 0, + WaitP95: 0, + HandleP95: 0, + Occupancy: 0, + DigitalShare: 0, + }, + breakdowns: { + by_channel: {}, + }, + coverage: { + metric_status: {}, + supported_channels: {}, + exact_rows: 0, + total_rows: 0, + note: null, + metric_details: {}, + }, + filters: {}, + }; +} + +function emptyAiAnalyticsOverview(fromTs = null, toTs = null, channel = 'all', queueId = null) { + return { + window: { + from_ts: fromTs, + to_ts: toTs, + }, + filters: { + from_ts: fromTs, + to_ts: toTs, + queue_id: queueId, + channel, + }, + totals: { + sessions_started: 0, + sessions_contained: 0, + sessions_handoff: 0, + sessions_closed: 0, + sessions_closed_without_operator: 0, + assistant_turns: 0, + }, + metrics: { + containment_rate: 0, + handoff_rate: 0, + ai_latency_avg_ms: null, + ai_latency_p95_ms: null, + closed_without_operator_rate: 0, + human_touched_rate: 0, + }, + breakdowns: { + by_channel: [], + by_outcome: [], + by_handoff_reason: [], + }, + coverage: { + sessions_with_interaction_id: 0, + sessions_with_queue_id: 0, + sessions_with_latency_turns: 0, + sessions_with_terminal_state: 0, + sessions_with_handoff_reason: 0, + }, + }; +} + +function emptyAiAnalyticsTimeseries(metric = 'containment_rate', interval = 'day', fromTs = null, toTs = null, channel = 'all', queueId = null) { + return { + metric, + interval, + filters: { + from_ts: fromTs, + to_ts: toTs, + queue_id: queueId, + channel, + }, + points: [], + }; +} + +function emptyAgentAnalyticsOverview(fromTs = null, toTs = null, channel = 'all', queueId = null) { + return { + window: { + from_ts: fromTs, + to_ts: toTs, + }, + filters: { + from_ts: fromTs, + to_ts: toTs, + queue_id: queueId, + channel, + sort_by: 'interactions_total', + sort_dir: 'desc', + limit: 25, + }, + totals: { + agents_total: 0, + agents_with_activity: 0, + interactions_total: 0, + answered_total: 0, + closed_total: 0, + ready_now: 0, + busy_now: 0, + break_now: 0, + offline_now: 0, + avg_interactions_per_agent: 0, + avg_handle_seconds: null, + avg_fcr_rate: null, + }, + state_snapshot: { + by_state: {}, + updated_at: null, + }, + breakdowns: { + by_team: [], + by_shift: [], + }, + items: [], + }; +} + +function emptyAgentAnalyticsTimeseries(metric = 'interactions_per_agent', interval = 'day') { + return { + metric, + interval, + filters: { + from_ts: null, + to_ts: null, + queue_id: null, + channel: 'all', + metric, + interval, + }, + points: [], + }; +} + +function emptyVoiceNameAnalyticsOverview(fromTs = null, toTs = null, queueId = null, language = null) { + return { + window: { + from_ts: fromTs, + to_ts: toTs, + }, + filters: { + from_ts: fromTs, + to_ts: toTs, + queue_id: queueId, + language, + }, + totals: { + scenario_calls: 0, + start_obtained: 0, + downstream_ai_obtained: 0, + followup_required: 0, + name_not_obtained: 0, + manual_corrected: 0, + handoff_confirmed_name: 0, + handoff_unconfirmed_name: 0, + needed_downstream: 0, + }, + metrics: { + start_capture_rate: 0, + downstream_rescue_rate: 0, + handoff_unconfirmed_rate: 0, + manual_correction_rate: 0, + }, + breakdowns: { + funnel: [], + by_language: [], + by_queue: [], + handoff: [], + }, + coverage: { + sessions_with_start_decision: 0, + sessions_with_final_ai_state: 0, + sessions_with_manual_overlay: 0, + note: null, + }, + }; +} + +function emptyVoiceNameAnalyticsTimeseries(metric = 'scenario_calls', interval = 'day', fromTs = null, toTs = null, queueId = null, language = null) { + return { + metric, + interval, + filters: { + from_ts: fromTs, + to_ts: toTs, + queue_id: queueId, + language, + }, + points: [], + }; +} + +function emptyAnalyticsMetricCoverage() { + return { + status: 'unavailable', + supported_channels: [], + exact_rows: 0, + total_rows: 0, + note: null, + }; +} + +function emptyDrilldownState() { + return { + open: false, + mode: 'interaction', + sourceType: '', + sourceLabel: '', + sourceValue: '', + metric: '', + filters: null, + coverage: null, + metricNote: '', + items: [], + total: 0, + limit: DRILLDOWN_PAGE_SIZE, + offset: 0, + selectedInteractionId: '', + selectedInteraction: null, + selectedLinkedInteraction: null, + selectedTimeline: [], + loading: false, + detailLoading: false, + exporting: false, + notice: '', + error: '', + }; +} + +function pad2(value) { + return String(value).padStart(2, '0'); +} + +function startOfDay(date) { + const next = new Date(date); + next.setHours(0, 0, 0, 0); + return next; +} + +function addDays(date, days) { + const next = new Date(date); + next.setDate(next.getDate() + days); + return next; +} + +function normalizeAnalyticsRange(from, to) { + const safeFrom = new Date(from); + const safeTo = new Date(to); + if (Number.isNaN(safeFrom.getTime()) || Number.isNaN(safeTo.getTime())) { + return { from: new Date(), to: new Date() }; + } + if (safeTo <= safeFrom) { + return { from: safeFrom, to: new Date(safeFrom.getTime() + 60 * 60 * 1000) }; + } + return { from: safeFrom, to: safeTo }; +} + +function analyticsRangeFromControls() { + const preset = $('analyticsPreset').value || state.analytics.preset || '7d'; + const customFrom = $('analyticsFrom').value.trim(); + const customTo = $('analyticsTo').value.trim(); + const now = new Date(); + let from; + let to; + let custom = false; + + if (customFrom && customTo) { + custom = true; + from = new Date(customFrom); + to = new Date(customTo); + } else if (preset === 'today') { + from = startOfDay(now); + to = now; + } else if (preset === '30d') { + from = startOfDay(addDays(now, -29)); + to = now; + } else { + from = startOfDay(addDays(now, -6)); + to = now; + } + + const current = normalizeAnalyticsRange(from, to); + const durationMs = Math.max(current.to.getTime() - current.from.getTime(), 60 * 60 * 1000); + const previous = { + from: new Date(current.from.getTime() - durationMs), + to: new Date(current.from.getTime()), + }; + + return { + preset, + custom, + current, + previous, + }; +} + +function syncAnalyticsStateFromControls() { + state.analytics.preset = $('analyticsPreset').value || '7d'; + state.analytics.fromTs = $('analyticsFrom').value.trim(); + state.analytics.toTs = $('analyticsTo').value.trim(); + state.analytics.queueId = $('analyticsQueue').value || 'all'; + state.analytics.channel = $('analyticsChannel').value || 'all'; + state.analytics.compareMode = $('analyticsCompareMode').value || 'previous'; + state.analytics.trendMetric = $('analyticsTrendMetric').value || 'volume'; + state.analytics.voiceNameTrendMetric = $('voiceNameAnalyticsTrendMetric')?.value || 'scenario_calls'; + state.analytics.aiTrendMetric = $('aiAnalyticsTrendMetric')?.value || 'containment_rate'; + state.analytics.agentTrendMetric = $('agentAnalyticsTrendMetric')?.value || 'interactions_per_agent'; +} + +function syncAnalyticsControlsFromState() { + $('analyticsPreset').value = state.analytics.preset || '7d'; + $('analyticsFrom').value = state.analytics.fromTs || ''; + $('analyticsTo').value = state.analytics.toTs || ''; + $('analyticsChannel').value = state.analytics.channel || 'all'; + $('analyticsCompareMode').value = state.analytics.compareMode || 'previous'; + $('analyticsTrendMetric').value = state.analytics.trendMetric || 'volume'; + if ($('voiceNameAnalyticsTrendMetric')) { + $('voiceNameAnalyticsTrendMetric').value = state.analytics.voiceNameTrendMetric || 'scenario_calls'; + } + if ($('aiAnalyticsTrendMetric')) { + $('aiAnalyticsTrendMetric').value = state.analytics.aiTrendMetric || 'containment_rate'; + } + if ($('agentAnalyticsTrendMetric')) { + $('agentAnalyticsTrendMetric').value = state.analytics.agentTrendMetric || 'interactions_per_agent'; + } + renderAnalyticsQueueOptions(); + renderSavedAnalyticsViews(); +} + +function resetAnalyticsFilters() { + state.analytics.preset = '7d'; + state.analytics.fromTs = ''; + state.analytics.toTs = ''; + state.analytics.queueId = 'all'; + state.analytics.channel = 'all'; + state.analytics.compareMode = 'previous'; + state.analytics.trendMetric = 'volume'; + state.analytics.voiceNameTrendMetric = 'scenario_calls'; + state.analytics.aiTrendMetric = 'containment_rate'; + state.analytics.agentTrendMetric = 'interactions_per_agent'; + state.analytics.activeViewId = ''; + state.analytics.trendInterval = 'day'; + state.analytics.agentTrendInterval = 'day'; + state.analytics.statusMessage = ''; + resetAnalyticsDrilldown(); + syncAnalyticsControlsFromState(); + syncAnalyticsUrlState(); +} + +function analyticsCurrentSnapshot() { + return { + preset: state.analytics.preset || '7d', + fromTs: state.analytics.fromTs || '', + toTs: state.analytics.toTs || '', + queueId: state.analytics.queueId || 'all', + channel: state.analytics.channel || 'all', + compareMode: state.analytics.compareMode || 'previous', + trendMetric: state.analytics.trendMetric || 'volume', + voiceNameTrendMetric: state.analytics.voiceNameTrendMetric || 'scenario_calls', + aiTrendMetric: state.analytics.aiTrendMetric || 'containment_rate', + agentTrendMetric: state.analytics.agentTrendMetric || 'interactions_per_agent', + }; +} + +function analyticsMockEnabled() { + return ENABLE_ANALYTICS_MOCK_DATA === true; +} + +function analyticsMockData() { + return state.analytics.mockData || {}; +} + +async function ensureAnalyticsMockDataLoaded() { + if (!analyticsMockEnabled()) { + return null; + } + if (state.analytics.mockData) { + return state.analytics.mockData; + } + try { + const response = await fetch(ANALYTICS_MOCK_DATA_URL, { cache: 'no-store' }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const payload = await response.json(); + state.analytics.mockData = payload && typeof payload === 'object' ? payload : {}; + state.analytics.mockDataError = ''; + return state.analytics.mockData; + } catch (error) { + state.analytics.mockData = {}; + state.analytics.mockDataError = error?.message || 'Не удалось загрузить mock-analytics.json'; + return state.analytics.mockData; + } +} + +function mockAnalyticsScale(range) { + if (!range?.from || !range?.to) { + return 1; + } + const dayMs = 24 * 60 * 60 * 1000; + const duration = Math.max(range.to.getTime() - range.from.getTime(), dayMs); + return Math.max(0.6, duration / (7 * dayMs)); +} + +function mockAnalyticsQueueOptions() { + const queueOptions = Array.isArray(analyticsMockData().queue_options) + ? analyticsMockData().queue_options + : []; + return queueOptions.map((item) => ({ ...item })); +} + +function mockAnalyticsAgents() { + return Array.isArray(analyticsMockData().agents) + ? analyticsMockData().agents.slice() + : []; +} + +function mockAnalyticsQueueFactor(queueId = state.analytics.queueId) { + const factors = analyticsMockData().queue_factors || {}; + if (!queueId || queueId === 'all') { + return 1; + } + return factors[queueId] || 0.84; +} + +function mockAnalyticsChannelBase() { + return analyticsMockData().channel_base || {}; +} + +function mockAiReasonLabel(reasonKey) { + const labels = analyticsMockData().ai_reason_labels || {}; + return labels[reasonKey] || reasonKey || 'Другая причина'; +} + +function mockInteractionStatus(index = 0) { + const statuses = Array.isArray(analyticsMockData().interaction_statuses) + ? analyticsMockData().interaction_statuses + : []; + if (!statuses.length) { + return 'closed'; + } + return statuses[index % statuses.length]; +} + +function mockAnalyticsCoverage(totalRows, note = analyticsMockData().coverage_note || 'Показаны демонстрационные данные для временного наполнения витрины.') { + const metricNames = ['total', 'answered', 'SL', 'ASA', 'AHT', 'Abandon', 'FCR', 'DigitalShare']; + const metricStatus = {}; + const supportedChannels = {}; + const metricDetails = {}; + metricNames.forEach((metric) => { + metricStatus[metric] = 'exact'; + supportedChannels[metric] = ['voice', 'telegram', 'whatsapp', 'webchat', 'email']; + metricDetails[metric] = { + status: 'exact', + supported_channels: supportedChannels[metric], + exact_rows: totalRows, + total_rows: totalRows, + note, + }; + }); + return { + metric_status: metricStatus, + supported_channels: supportedChannels, + exact_rows: totalRows, + total_rows: totalRows, + note, + metric_details: metricDetails, + }; +} + +function buildMockAnalyticsOverview(rangeMeta, options = {}) { + const previous = Boolean(options.previous); + const queueId = options.queueId ?? state.analytics.queueId; + const channel = options.channel ?? state.analytics.channel; + const scale = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) * mockAnalyticsQueueFactor(queueId) * (previous ? 0.89 : 1); + const baseByChannel = mockAnalyticsChannelBase(); + const channelKeys = channel && channel !== 'all' ? [channel] : Object.keys(baseByChannel); + const byChannel = {}; + channelKeys.forEach((key) => { + const base = baseByChannel[key] || { total: 0, answered: 0, abandoned: 0 }; + byChannel[key] = { + total: Math.round(base.total * scale), + answered: Math.round(base.answered * scale), + abandoned: Math.round(base.abandoned * scale), + }; + }); + const totals = Object.values(byChannel).reduce((acc, item) => ({ + total: acc.total + Number(item.total || 0), + answered: acc.answered + Number(item.answered || 0), + abandoned: acc.abandoned + Number(item.abandoned || 0), + }), { total: 0, answered: 0, abandoned: 0 }); + const digitalTotal = Object.entries(byChannel) + .filter(([key]) => key !== 'voice') + .reduce((acc, [, item]) => acc + Number(item.total || 0), 0); + const answerRate = totals.total ? (totals.answered / totals.total) * 100 : 0; + const abandonRate = totals.total ? (totals.abandoned / totals.total) * 100 : 0; + const sl = Math.max(58, Math.min(96, 84 - (queueId === 'vip_kz' ? 4 : 0) - (channel === 'email' ? 9 : 0) + (previous ? -3 : 0))); + const asa = Math.max(10, 22 + (queueId === 'support_kz' ? 6 : 0) + (channel === 'email' ? 12 : channel === 'voice' ? 3 : 0) + (previous ? 4 : 0)); + const aht = Math.max(120, 332 + (queueId === 'vip_kz' ? 86 : 0) + (channel === 'email' ? 112 : channel === 'voice' ? 38 : 0) + (previous ? 24 : 0)); + const fcr = Math.max(48, Math.min(92, 73 - (queueId === 'support_kz' ? 5 : 0) + (channel === 'telegram' ? 4 : 0) + (previous ? -2 : 0))); + return { + window: { + from: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + }, + volume: totals, + kpi: { + SL: Number(sl.toFixed(2)), + ASA: Number(asa.toFixed(2)), + AHT: Number(aht.toFixed(2)), + Abandon: Number(abandonRate.toFixed(2)), + FCR: Number(fcr.toFixed(2)), + AnswerRate: Number(answerRate.toFixed(2)), + WaitP95: Number((asa * 2.6).toFixed(2)), + HandleP95: Number((aht * 1.9).toFixed(2)), + Occupancy: Number((66 + (queueId === 'support_kz' ? 8 : 0) + (previous ? -3 : 0)).toFixed(2)), + DigitalShare: totals.total ? Number(((digitalTotal / totals.total) * 100).toFixed(2)) : 0, + }, + breakdowns: { + by_channel: byChannel, + }, + coverage: mockAnalyticsCoverage(totals.total), + filters: { + queue_id: queueId === 'all' ? null : queueId, + channel, + }, + }; +} + +function buildMockTrendPayloads(rangeMeta, metric) { + const overview = buildMockAnalyticsOverview(rangeMeta); + const baseValue = metricValueFromPayload(metric, overview); + const buckets = buildTrendBuckets(rangeMeta.current, rangeMeta.preset, rangeMeta.custom); + const count = Math.max(buckets.length, 1); + return buckets.map((bucket, index) => { + const wave = 0.88 + (((index % 5) - 2) * 0.06) + (index / Math.max(count - 1, 1)) * 0.12; + const nextValue = Math.max(0, baseValue * wave); + return { + label: bucket.label, + range: bucket.range, + payload: metric === 'volume' || metric === 'total' + ? { + ...emptyKpiEnvelope(bucket.range.from.toISOString(), bucket.range.to.toISOString()), + volume: { + total: Math.round(nextValue), + answered: Math.round(nextValue * 0.92), + abandoned: Math.round(nextValue * 0.08), + }, + kpi: { + ...emptyKpiEnvelope().kpi, + SL: 82 + (index % 4), + ASA: 16 + index, + AHT: 320 + index * 8, + Abandon: 6 + (index % 3), + FCR: 72 + (index % 5), + DigitalShare: 44 + (index % 4), + }, + } + : { + ...emptyKpiEnvelope(bucket.range.from.toISOString(), bucket.range.to.toISOString()), + volume: { + total: Math.round(overview.volume.total / count), + answered: Math.round((overview.volume.answered || 0) / count), + abandoned: Math.round((overview.volume.abandoned || 0) / count), + }, + kpi: { + ...emptyKpiEnvelope().kpi, + [metric]: Number(nextValue.toFixed(2)), + }, + }, + }; + }); +} + +function buildMockQueueRows(queueOptions, rangeMeta) { + const overview = buildMockAnalyticsOverview(rangeMeta); + const channel = state.analytics.channel; + return queueOptions.map((queue, index) => { + const factor = mockAnalyticsQueueFactor(queue.queue_id) * (channel === 'all' ? 1 : 0.82); + const total = Math.round(overview.volume.total * factor * (0.34 + index * 0.11)); + const answered = Math.round(total * (0.91 - index * 0.03)); + return { + queue_id: queue.queue_id, + name: queue.name, + total, + answered, + SL: Number((84 - index * 4).toFixed(2)), + Abandon: Number((6 + index * 1.4).toFixed(2)), + }; + }).sort((a, b) => b.total - a.total); +} + +function buildMockAgentOverview(rangeMeta, options = {}) { + const previous = Boolean(options.previous); + const queueId = options.queueId ?? state.analytics.queueId; + const queueOptions = mockAnalyticsQueueOptions(); + const agents = mockAnalyticsAgents(); + const stateCycle = Array.isArray(analyticsMockData().agent_state_cycle) + ? analyticsMockData().agent_state_cycle + : []; + const rows = agents.map((agentId, index) => { + const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz', name: 'Продажи KZ' }; + const dominantQueue = queueId !== 'all' ? queueId : queue.queue_id; + const interactionsTotal = Math.round((24 - index * 2) * (previous ? 0.88 : 1)); + const answeredTotal = Math.max(0, Math.round(interactionsTotal * (0.88 - index * 0.015))); + return { + agent_id: agentId, + interactions_total: interactionsTotal, + answered_total: answeredTotal, + closed_total: Math.max(0, answeredTotal - (index % 3)), + avg_handle_seconds: Number((290 + index * 24 + (previous ? 18 : 0)).toFixed(2)), + fcr_rate: Number((78 - index * 3 + (previous ? -2 : 0)).toFixed(2)), + current_state: stateCycle.length ? stateCycle[index % stateCycle.length] : 'READY', + current_queue_id: dominantQueue, + dominant_queue_id: dominantQueue, + last_activity_at: new Date(rangeMeta.current.to.getTime() - index * 37 * 60 * 1000).toISOString(), + }; + }).filter((item) => queueId === 'all' || item.current_queue_id === queueId); + const agentsWithActivity = rows.filter((item) => item.interactions_total > 0).length; + const interactionsTotal = rows.reduce((acc, item) => acc + item.interactions_total, 0); + const answeredTotal = rows.reduce((acc, item) => acc + item.answered_total, 0); + const readyNow = rows.filter((item) => item.current_state === 'READY').length; + const busyNow = rows.filter((item) => item.current_state === 'BUSY').length; + const breakNow = rows.filter((item) => item.current_state === 'BREAK').length; + const offlineNow = rows.filter((item) => item.current_state === 'OFFLINE').length; + const byTeam = mockAnalyticsQueueOptions() + .filter((queue) => queueId === 'all' || queue.queue_id === queueId) + .map((queue, index) => { + const teamRows = rows.filter((item) => item.current_queue_id === queue.queue_id); + const teamInteractions = teamRows.reduce((acc, item) => acc + item.interactions_total, 0); + return { + team_key: queue.queue_id, + label: queue.name, + agents_total: teamRows.length, + agents_with_activity: teamRows.filter((item) => item.interactions_total > 0).length, + interactions_total: teamInteractions, + answered_total: teamRows.reduce((acc, item) => acc + item.answered_total, 0), + avg_handle_seconds: teamRows.length ? Number((312 + index * 22).toFixed(2)) : null, + fcr_rate: teamRows.length ? Number((76 - index * 4).toFixed(2)) : null, + ready_now: teamRows.filter((item) => item.current_state === 'READY').length, + busy_now: teamRows.filter((item) => item.current_state === 'BUSY').length, + break_now: teamRows.filter((item) => item.current_state === 'BREAK').length, + offline_now: teamRows.filter((item) => item.current_state === 'OFFLINE').length, + }; + }) + .filter((item) => item.agents_total > 0); + const shiftSeeds = Array.isArray(analyticsMockData().agent_shift_rows) + ? analyticsMockData().agent_shift_rows + : []; + const byShift = shiftSeeds.map((seed) => ({ + shift_key: seed.shift_key, + label: seed.label, + agents_with_activity: Math.max(1, Math.round(Number(seed.agents_with_activity || 0) * (previous ? 0.92 : 1))), + interactions_total: Math.max(0, Math.round(Number(seed.interactions_total || 0) * (previous ? 0.9 : 1))), + answered_total: Math.max(0, Math.round(Number(seed.answered_total || 0) * (previous ? 0.9 : 1))), + avg_handle_seconds: Number((Number(seed.avg_handle_seconds || 0) + (previous ? 18 : 0)).toFixed(2)), + fcr_rate: Number((Number(seed.fcr_rate || 0) + (previous ? -2 : 0)).toFixed(2)), + })); + return { + window: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + }, + filters: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + queue_id: queueId === 'all' ? null : queueId, + channel: state.analytics.channel, + sort_by: 'interactions_total', + sort_dir: 'desc', + limit: 25, + }, + totals: { + agents_total: rows.length, + agents_with_activity: agentsWithActivity, + interactions_total: interactionsTotal, + answered_total: answeredTotal, + closed_total: rows.reduce((acc, item) => acc + item.closed_total, 0), + ready_now: readyNow, + busy_now: busyNow, + break_now: breakNow, + offline_now: offlineNow, + avg_interactions_per_agent: rows.length ? Number((interactionsTotal / rows.length).toFixed(2)) : 0, + avg_handle_seconds: rows.length ? Number((rows.reduce((acc, item) => acc + Number(item.avg_handle_seconds || 0), 0) / rows.length).toFixed(2)) : null, + avg_fcr_rate: rows.length ? Number((rows.reduce((acc, item) => acc + Number(item.fcr_rate || 0), 0) / rows.length).toFixed(2)) : null, + }, + state_snapshot: { + by_state: { + READY: readyNow, + BUSY: busyNow, + BREAK: breakNow, + OFFLINE: offlineNow, + }, + updated_at: rangeMeta.current.to.toISOString(), + }, + breakdowns: { + by_team: byTeam, + by_shift: byShift, + }, + items: rows, + }; +} + +function buildMockAgentTrend(rangeMeta, metric) { + const interval = analyticsTimeseriesIntervalForRange(rangeMeta); + const points = []; + let cursor = new Date(interval === 'hour' + ? rangeMeta.current.from.getTime() + : startOfDay(rangeMeta.current.from).getTime()); + while (cursor < rangeMeta.current.to) { + const index = points.length; + let value = 0; + if (metric === 'agents_with_activity') { + value = 5 + (index % 4); + } else if (metric === 'avg_handle_seconds') { + value = 296 + index * 8; + } else if (metric === 'fcr_rate') { + value = 69 + (index % 5) * 2.2; + } else { + value = 3.4 + (index % 4) * 0.42; + } + points.push({ + ts: cursor.toISOString(), + value: Number(value.toFixed(2)), + agents_with_activity: 5 + (index % 3), + interactions_total: 18 + index * 2, + }); + cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000)); + } + return { + metric, + interval, + filters: { + from_ts: rangeMeta.current.from.toISOString(), + to_ts: rangeMeta.current.to.toISOString(), + queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId, + channel: state.analytics.channel, + metric, + interval, + }, + points, + }; +} + +function buildMockAiOverview(rangeMeta, options = {}) { + const previous = Boolean(options.previous); + const channel = options.channel ?? state.analytics.channel; + if (!aiAnalyticsSupportedChannel(channel)) { + return emptyAiAnalyticsOverview( + (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + } + const scale = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) * (previous ? 0.91 : 1); + const aiChannelRows = Array.isArray(analyticsMockData().ai_channel_rows) + ? analyticsMockData().ai_channel_rows + : []; + const baseRows = aiChannelRows.map((item) => ({ + ...item, + sessions_started: Math.round(Number(item.sessions_started || 0) * scale), + ai_only_sessions: Math.round(Number(item.ai_only_sessions || 0) * scale), + human_touched_sessions: Math.round(Number(item.human_touched_sessions || 0) * scale), + })).filter((item) => channel === 'all' || item.channel === channel); + const sessionsStarted = baseRows.reduce((acc, item) => acc + item.sessions_started, 0); + const sessionsContained = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.containment_rate / 100), 0)); + const sessionsHandoff = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.handoff_rate / 100), 0)); + const sessionsClosedWithoutOperator = Math.round(baseRows.reduce((acc, item) => acc + item.sessions_started * (item.closed_without_operator_rate / 100), 0)); + const humanTouched = baseRows.reduce((acc, item) => acc + item.human_touched_sessions, 0); + const byOutcome = [ + { outcome: 'contained', label: 'Закрыто AI', sessions: sessionsContained, share: sessionsStarted ? (sessionsContained / sessionsStarted) * 100 : 0 }, + { outcome: 'handoff', label: 'Передано оператору', sessions: sessionsHandoff, share: sessionsStarted ? (sessionsHandoff / sessionsStarted) * 100 : 0 }, + { outcome: 'human_touched', label: 'С участием оператора', sessions: humanTouched, share: sessionsStarted ? (humanTouched / sessionsStarted) * 100 : 0 }, + { outcome: 'closed_without_operator', label: 'Закрыто без оператора', sessions: sessionsClosedWithoutOperator, share: sessionsStarted ? (sessionsClosedWithoutOperator / sessionsStarted) * 100 : 0 }, + { outcome: 'active', label: 'Активные', sessions: Math.max(8, Math.round(sessionsStarted * 0.06)), share: 6.1 }, + { outcome: 'error', label: 'Ошибки', sessions: Math.max(3, Math.round(sessionsStarted * 0.02)), share: 1.9 }, + ]; + const byReason = [ + 'requested_human', + 'knowledge_or_tool_gap', + 'policy_or_sensitive', + 'delivery_or_runtime_error', + 'manual_claim', + ].map((reasonKey, index) => ({ + reason_key: reasonKey, + label: mockAiReasonLabel(reasonKey), + sessions: Math.max(4, Math.round(sessionsHandoff * (0.34 - index * 0.05))), + share: Math.max(6, Number((32 - index * 5.2).toFixed(2))), + })); + return { + window: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + }, + filters: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId, + channel, + }, + totals: { + sessions_started: sessionsStarted, + sessions_contained: sessionsContained, + sessions_handoff: sessionsHandoff, + sessions_closed: Math.round(sessionsStarted * 0.86), + sessions_closed_without_operator: sessionsClosedWithoutOperator, + assistant_turns: Math.round(sessionsStarted * 5.4), + }, + metrics: { + containment_rate: sessionsStarted ? Number(((sessionsContained / sessionsStarted) * 100).toFixed(2)) : 0, + handoff_rate: sessionsStarted ? Number(((sessionsHandoff / sessionsStarted) * 100).toFixed(2)) : 0, + ai_latency_avg_ms: 1452, + ai_latency_p95_ms: 2486, + closed_without_operator_rate: sessionsStarted ? Number(((sessionsClosedWithoutOperator / sessionsStarted) * 100).toFixed(2)) : 0, + human_touched_rate: sessionsStarted ? Number(((humanTouched / sessionsStarted) * 100).toFixed(2)) : 0, + }, + breakdowns: { + by_channel: baseRows, + by_outcome: byOutcome, + by_handoff_reason: byReason, + }, + coverage: { + sessions_with_interaction_id: Math.round(sessionsStarted * 0.94), + sessions_with_queue_id: Math.round(sessionsStarted * 0.91), + sessions_with_latency_turns: Math.round(sessionsStarted * 0.97), + sessions_with_terminal_state: Math.round(sessionsStarted * 0.88), + sessions_with_handoff_reason: Math.round(sessionsHandoff * 0.82), + }, + }; +} + +function buildMockAiTrend(rangeMeta, metric) { + const interval = aiAnalyticsIntervalForRange(rangeMeta); + const points = []; + let cursor = new Date(interval === 'hour' + ? rangeMeta.current.from.getTime() + : startOfDay(rangeMeta.current.from).getTime()); + while (cursor < rangeMeta.current.to) { + const index = points.length; + let value = 0; + if (metric === 'handoff_rate') { + value = 21 + (index % 5) * 1.8; + } else if (metric === 'human_touched_rate') { + value = 32 + (index % 4) * 1.6; + } else if (metric === 'ai_latency_avg_ms') { + value = 1380 + index * 34; + } else if (metric === 'closed_without_operator_rate') { + value = 47 + (index % 4) * 1.9; + } else { + value = 54 + (index % 5) * 2.1; + } + points.push({ + ts: cursor.toISOString(), + value: Number(value.toFixed(2)), + sessions: 24 + index * 3, + assistant_turns: 118 + index * 11, + }); + cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000)); + } + return { + metric, + interval, + filters: { + from_ts: rangeMeta.current.from.toISOString(), + to_ts: rangeMeta.current.to.toISOString(), + queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId, + channel: state.analytics.channel, + }, + points, + }; +} + +function mockVoiceNameSeed() { + const seed = analyticsMockData().voice_name_flow; + return seed && typeof seed === 'object' ? seed : {}; +} + +function mockVoiceNameLabels() { + const labels = analyticsMockData().voice_name_labels; + return labels && typeof labels === 'object' ? labels : {}; +} + +function mockVoiceNameLabel(key, fallback) { + return mockVoiceNameLabels()[key] || fallback; +} + +function mockVoiceNameProfile(metric, fallback) { + const profile = mockVoiceNameSeed().trend_profiles?.[metric]; + if (!profile || typeof profile !== 'object') { + return fallback; + } + return { + base: Number.isFinite(Number(profile.base)) ? Number(profile.base) : fallback.base, + step: Number.isFinite(Number(profile.step)) ? Number(profile.step) : fallback.step, + cycle: Math.max(1, Number.isFinite(Number(profile.cycle)) ? Number(profile.cycle) : fallback.cycle), + }; +} + +function voiceNameMetricRatesFromTotals(totals) { + const scenarioCalls = Number(totals.scenario_calls || 0); + const neededDownstream = Number(totals.needed_downstream || Math.max(0, scenarioCalls - Number(totals.start_obtained || 0))); + const allHandoffs = Number(totals.handoff_confirmed_name || 0) + Number(totals.handoff_unconfirmed_name || 0); + return { + start_capture_rate: scenarioCalls ? Number(((Number(totals.start_obtained || 0) / scenarioCalls) * 100).toFixed(2)) : 0, + downstream_rescue_rate: neededDownstream ? Number(((Number(totals.downstream_ai_obtained || 0) / neededDownstream) * 100).toFixed(2)) : 0, + handoff_unconfirmed_rate: allHandoffs ? Number(((Number(totals.handoff_unconfirmed_name || 0) / allHandoffs) * 100).toFixed(2)) : 0, + manual_correction_rate: allHandoffs ? Number(((Number(totals.manual_corrected || 0) / allHandoffs) * 100).toFixed(2)) : 0, + }; +} + +function buildMockVoiceNameOverview(rangeMeta, options = {}) { + const previous = Boolean(options.previous); + const channel = options.channel ?? state.analytics.channel; + if (!(channel === 'all' || channel === 'voice')) { + return emptyVoiceNameAnalyticsOverview( + (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + } + const seed = mockVoiceNameSeed(); + const totalSeed = seed.totals || {}; + const queueId = options.queueId ?? state.analytics.queueId; + const factor = mockAnalyticsScale(previous ? rangeMeta.previous : rangeMeta.current) + * (previous ? 0.91 : 1) + * (queueId === 'all' ? 1 : Math.max(0.48, mockAnalyticsQueueFactor(queueId))); + const baseScenarioCalls = Math.max(Number(totalSeed.scenario_calls || 180), 1); + const scenarioCalls = Math.max(0, Math.round(baseScenarioCalls * factor)); + const startRatio = Number(totalSeed.start_obtained || 110) / baseScenarioCalls; + const downstreamRatio = Number(totalSeed.downstream_ai_obtained || 36) / baseScenarioCalls; + const followupRatio = Number(totalSeed.followup_required || 20) / baseScenarioCalls; + const handoffConfirmedRatio = Number(totalSeed.handoff_confirmed_name || 12) / baseScenarioCalls; + const handoffUnconfirmedRatio = Number(totalSeed.handoff_unconfirmed_name || 26) / baseScenarioCalls; + const manualRateSeed = Number(totalSeed.manual_corrected || 10) + / Math.max((Number(totalSeed.handoff_confirmed_name || 12) + Number(totalSeed.handoff_unconfirmed_name || 26)), 1); + + const startObtained = Math.min(scenarioCalls, Math.round(scenarioCalls * startRatio)); + const downstreamAiObtained = Math.max(0, Math.round(scenarioCalls * downstreamRatio)); + const followupRequired = Math.max(0, Math.round(scenarioCalls * followupRatio)); + const nameNotObtained = Math.max(0, scenarioCalls - startObtained - downstreamAiObtained - followupRequired); + const neededDownstream = Math.max(0, scenarioCalls - startObtained); + const handoffConfirmedName = Math.max(0, Math.round(scenarioCalls * handoffConfirmedRatio)); + const handoffUnconfirmedName = Math.max(0, Math.round(scenarioCalls * handoffUnconfirmedRatio)); + const allHandoffs = Math.max(0, handoffConfirmedName + handoffUnconfirmedName); + const manualCorrected = Math.min(allHandoffs, Math.round(allHandoffs * manualRateSeed)); + const totals = { + scenario_calls: scenarioCalls, + start_obtained: startObtained, + downstream_ai_obtained: downstreamAiObtained, + followup_required: followupRequired, + name_not_obtained: nameNotObtained, + manual_corrected: manualCorrected, + handoff_confirmed_name: handoffConfirmedName, + handoff_unconfirmed_name: handoffUnconfirmedName, + needed_downstream: neededDownstream, + }; + const metrics = voiceNameMetricRatesFromTotals(totals); + + const languageSeeds = Array.isArray(seed.languages) ? seed.languages : []; + const languageRows = languageSeeds.map((item) => { + const share = Number(item.share || 0); + const calls = Math.max(0, Math.round(scenarioCalls * share)); + const startCaptured = Math.min(calls, Math.round(calls * Number(item.start_capture_rate || 0) / 100)); + const downstreamNeeded = Math.max(0, calls - startCaptured); + const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * Number(item.downstream_rescue_rate || 0) / 100)); + const unresolved = Math.max(0, calls - startCaptured - downstreamCaptured); + const followup = Math.round(unresolved * 0.58); + const missing = Math.max(0, unresolved - followup); + const handoffs = Math.round(unresolved * 0.72); + const unconfirmed = Math.min(handoffs, Math.round(handoffs * Number(item.handoff_unconfirmed_rate || 0) / 100)); + const confirmed = Math.max(0, handoffs - unconfirmed); + const manual = Math.min(handoffs, Math.round(handoffs * Number(item.manual_correction_rate || 0) / 100)); + return { + language: item.language || 'unknown', + scenario_calls: calls, + start_obtained: startCaptured, + downstream_ai_obtained: downstreamCaptured, + followup_required: followup, + name_not_obtained: missing, + manual_corrected: manual, + handoff_confirmed_name: confirmed, + handoff_unconfirmed_name: unconfirmed, + ...voiceNameMetricRatesFromTotals({ + scenario_calls: calls, + start_obtained: startCaptured, + downstream_ai_obtained: downstreamCaptured, + needed_downstream: downstreamNeeded, + handoff_confirmed_name: confirmed, + handoff_unconfirmed_name: unconfirmed, + manual_corrected: manual, + }), + }; + }).filter((item) => item.scenario_calls > 0); + + const queueSeeds = Array.isArray(seed.queues) ? seed.queues : []; + const queueRows = (queueSeeds.length ? queueSeeds : mockAnalyticsQueueOptions().map((item, index) => ({ + queue_id: item.queue_id, + share: 0.2 + index * 0.12, + start_capture_rate: 69 - index * 4, + downstream_rescue_rate: 45 - index * 5, + handoff_unconfirmed_rate: 38 + index * 5, + manual_correction_rate: 17 + index * 3, + }))) + .filter((item) => queueId === 'all' || item.queue_id === queueId) + .map((item) => { + const calls = Math.max(0, Math.round(scenarioCalls * Number(item.share || 0))); + const startCaptured = Math.min(calls, Math.round(calls * Number(item.start_capture_rate || 0) / 100)); + const downstreamNeeded = Math.max(0, calls - startCaptured); + const downstreamCaptured = Math.min(downstreamNeeded, Math.round(downstreamNeeded * Number(item.downstream_rescue_rate || 0) / 100)); + const unresolved = Math.max(0, calls - startCaptured - downstreamCaptured); + const followup = Math.round(unresolved * 0.58); + const missing = Math.max(0, unresolved - followup); + const handoffs = Math.round(unresolved * 0.72); + const unconfirmed = Math.min(handoffs, Math.round(handoffs * Number(item.handoff_unconfirmed_rate || 0) / 100)); + const confirmed = Math.max(0, handoffs - unconfirmed); + const manual = Math.min(handoffs, Math.round(handoffs * Number(item.manual_correction_rate || 0) / 100)); + return { + queue_id: item.queue_id, + scenario_calls: calls, + start_obtained: startCaptured, + downstream_ai_obtained: downstreamCaptured, + followup_required: followup, + name_not_obtained: missing, + manual_corrected: manual, + handoff_confirmed_name: confirmed, + handoff_unconfirmed_name: unconfirmed, + ...voiceNameMetricRatesFromTotals({ + scenario_calls: calls, + start_obtained: startCaptured, + downstream_ai_obtained: downstreamCaptured, + needed_downstream: downstreamNeeded, + handoff_confirmed_name: confirmed, + handoff_unconfirmed_name: unconfirmed, + manual_corrected: manual, + }), + }; + }) + .filter((item) => item.scenario_calls > 0); + + const coverageSeed = seed.coverage || {}; + const coverage = { + sessions_with_start_decision: Math.min( + scenarioCalls, + Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_start_decision || baseScenarioCalls) / baseScenarioCalls)), + ), + sessions_with_final_ai_state: Math.min( + scenarioCalls, + Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_final_ai_state || baseScenarioCalls) / baseScenarioCalls)), + ), + sessions_with_manual_overlay: Math.min( + scenarioCalls, + Math.round(scenarioCalls * (Number(coverageSeed.sessions_with_manual_overlay || manualCorrected) / baseScenarioCalls)), + ), + note: coverageSeed.note || analyticsMockData().coverage_note || 'Показаны демонстрационные данные по name-flow.', + }; + + return { + window: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + }, + filters: { + from_ts: (previous ? rangeMeta.previous.from : rangeMeta.current.from).toISOString(), + to_ts: (previous ? rangeMeta.previous.to : rangeMeta.current.to).toISOString(), + queue_id: queueId === 'all' ? null : queueId, + language: null, + }, + totals, + metrics, + breakdowns: { + funnel: [ + { stage: 'scenario_calls', label: mockVoiceNameLabel('scenario_calls', 'Звонки в сценарии'), sessions: scenarioCalls, share: 100 }, + { stage: 'start_obtained', label: mockVoiceNameLabel('start_obtained', 'Имя взято сразу'), sessions: startObtained, share: scenarioCalls ? Number(((startObtained / scenarioCalls) * 100).toFixed(2)) : 0 }, + { stage: 'needed_downstream', label: mockVoiceNameLabel('needed_downstream', 'Потребовался AI после старта'), sessions: neededDownstream, share: scenarioCalls ? Number(((neededDownstream / scenarioCalls) * 100).toFixed(2)) : 0 }, + { stage: 'downstream_ai_obtained', label: mockVoiceNameLabel('downstream_ai_obtained', 'Имя добрал AI после follow-up'), sessions: downstreamAiObtained, share: scenarioCalls ? Number(((downstreamAiObtained / scenarioCalls) * 100).toFixed(2)) : 0 }, + { stage: 'handoff_confirmed_name', label: mockVoiceNameLabel('handoff_confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: scenarioCalls ? Number(((handoffConfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 }, + { stage: 'handoff_unconfirmed_name', label: mockVoiceNameLabel('handoff_unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: scenarioCalls ? Number(((handoffUnconfirmedName / scenarioCalls) * 100).toFixed(2)) : 0 }, + ], + by_language: languageRows, + by_queue: queueRows, + handoff: [ + { outcome: 'confirmed_name', label: mockVoiceNameLabel('confirmed_name', 'Передача с подтверждённым именем'), sessions: handoffConfirmedName, share: allHandoffs ? Number(((handoffConfirmedName / allHandoffs) * 100).toFixed(2)) : 0 }, + { outcome: 'unconfirmed_name', label: mockVoiceNameLabel('unconfirmed_name', 'Передача без подтверждённого имени'), sessions: handoffUnconfirmedName, share: allHandoffs ? Number(((handoffUnconfirmedName / allHandoffs) * 100).toFixed(2)) : 0 }, + ], + }, + coverage, + }; +} + +function buildMockVoiceNameTrend(rangeMeta, metric) { + const interval = analyticsTimeseriesIntervalForRange(rangeMeta); + const scenarioProfile = mockVoiceNameProfile('scenario_calls', { base: 24, step: 2.4, cycle: 4 }); + const metricProfile = metric === 'scenario_calls' + ? scenarioProfile + : mockVoiceNameProfile(metric, { base: 60, step: 2.1, cycle: 4 }); + const queueFactor = Math.max(0.48, mockAnalyticsQueueFactor(state.analytics.queueId === 'all' ? 'sales_kz' : state.analytics.queueId)); + const points = []; + let cursor = new Date(interval === 'hour' + ? rangeMeta.current.from.getTime() + : startOfDay(rangeMeta.current.from).getTime()); + while (cursor < rangeMeta.current.to) { + const index = points.length; + const scenarioCalls = Math.max( + 4, + Math.round((scenarioProfile.base + (index % scenarioProfile.cycle) * scenarioProfile.step) * queueFactor), + ); + const neededDownstream = Math.max(1, Math.round(scenarioCalls * 0.35)); + const handoffs = Math.max(1, Math.round(scenarioCalls * 0.18)); + let value = 0; + let denominator = scenarioCalls; + if (metric === 'scenario_calls') { + value = scenarioCalls; + denominator = scenarioCalls; + } else if (metric === 'downstream_rescue_rate') { + value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step; + denominator = neededDownstream; + } else if (metric === 'handoff_unconfirmed_rate' || metric === 'manual_correction_rate') { + value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step; + denominator = handoffs; + } else { + value = metricProfile.base + (index % metricProfile.cycle) * metricProfile.step; + denominator = scenarioCalls; + } + points.push({ + ts: cursor.toISOString(), + value: Number(value.toFixed(metric === 'scenario_calls' ? 0 : 2)), + scenario_calls: scenarioCalls, + denominator, + }); + cursor = new Date(cursor.getTime() + (interval === 'hour' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000)); + } + return { + metric, + interval, + filters: { + from_ts: rangeMeta.current.from.toISOString(), + to_ts: rangeMeta.current.to.toISOString(), + queue_id: state.analytics.queueId === 'all' ? null : state.analytics.queueId, + language: null, + }, + points, + }; +} + +function buildMockInteractionDrilldownData(filters, limit, offset, mode) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const queueOptions = mockAnalyticsQueueOptions(); + const agents = mockAnalyticsAgents(); + const total = 18; + const rows = Array.from({ length: total }, (_, index) => { + const queueId = normalized.queue_id || queueOptions[index % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz'; + let channel = normalized.channel || ['voice', 'telegram', 'whatsapp', 'webchat', 'email'][index % 5]; + if (mode === 'metric' && normalized.metric === 'DigitalShare') { + channel = ['telegram', 'whatsapp', 'webchat', 'email'][index % 4]; + } + const answered = normalized.metric === 'Abandon' ? false : index % 5 !== 0; + const abandoned = normalized.metric === 'Abandon' ? true : index % 6 === 0; + const withinSla = normalized.metric === 'SL' ? true : answered ? index % 4 !== 0 : null; + const resolvedFirstContact = normalized.metric === 'FCR' ? true : answered ? index % 3 === 0 : null; + const createdAt = new Date(Date.now() - (index + 1) * 3 * 60 * 60 * 1000); + return { + interaction_id: `mock-int-${index + 1}`, + subject: `${mode === 'metric' ? 'KPI' : 'Обращение'} ${index + 1}: ${analyticsChannelLabel(channel)}`, + channel, + status: abandoned ? 'closed' : mockInteractionStatus(index), + queue_id: queueId, + assigned_to: normalized.agent_id || (abandoned ? null : (agents[index % Math.max(agents.length, 1)] || null)), + created_at: createdAt.toISOString(), + updated_at: new Date(createdAt.getTime() + 42 * 60 * 1000).toISOString(), + answered, + abandoned, + wait_seconds: abandoned ? 44 + index : 12 + index * 2, + handle_seconds: answered ? 210 + index * 14 : 0, + within_sla: withinSla, + resolved_first_contact: resolvedFirstContact, + }; + }).filter((item) => { + if (normalized.channel && item.channel !== normalized.channel) { + return false; + } + if (normalized.queue_id && item.queue_id !== normalized.queue_id) { + return false; + } + if (normalized.agent_id && item.assigned_to !== normalized.agent_id) { + return false; + } + return true; + }); + const paged = rows.slice(offset, offset + limit); + return { + items: paged, + total: rows.length, + limit, + offset, + filters: normalized, + metric: normalized.metric || '', + coverage: mockAnalyticsCoverage(rows.length), + }; +} + +function buildMockInteractionDetail(interactionId) { + const queueOptions = mockAnalyticsQueueOptions(); + const agents = mockAnalyticsAgents(); + const interaction = { + interaction_id: interactionId, + subject: `Карточка ${interactionId}`, + channel: ['voice', 'telegram', 'whatsapp'][interactionId.length % 3], + status: 'closed', + queue_id: queueOptions[interactionId.length % Math.max(queueOptions.length, 1)]?.queue_id || 'sales_kz', + assigned_to: agents[interactionId.length % Math.max(agents.length, 1)] || null, + created_at: new Date(Date.now() - 8 * 60 * 60 * 1000).toISOString(), + updated_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + wait_seconds: 18, + handle_seconds: 324, + within_sla: true, + resolved_first_contact: true, + abandoned: false, + }; + const timeline = { + events: [ + { action: 'Создано обращение', timestamp: interaction.created_at, metadata: { channel: interaction.channel, queue_id: interaction.queue_id } }, + { action: 'Назначено оператору', timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString(), metadata: { assigned_to: interaction.assigned_to } }, + { action: 'Закрыто', timestamp: interaction.updated_at, metadata: { resolved_first_contact: true, within_sla: true } }, + ], + }; + return { detail: interaction, timeline }; +} + +function buildMockAiDrilldownData(filters, limit, offset) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const queueOptions = mockAnalyticsQueueOptions(); + const agents = mockAnalyticsAgents(); + const total = 14; + const allItems = Array.from({ length: total }, (_, index) => { + const queue = queueOptions[index % Math.max(queueOptions.length, 1)] || { queue_id: 'sales_kz' }; + const channel = normalized.channel && normalized.channel !== 'all' + ? normalized.channel + : ['telegram', 'whatsapp'][index % 2]; + const slice = normalized.slice || 'all'; + const reasonKey = normalized.reason_key || ['requested_human', 'knowledge_or_tool_gap', 'manual_claim'][index % 3]; + const handoff = slice === 'handoff' || slice === 'human_touched' || index % 4 === 0; + const contained = slice === 'contained' || (!handoff && index % 2 === 0); + const humanTouched = slice === 'human_touched' || handoff; + const closedWithoutOperator = slice === 'closed_without_operator' || (contained && index % 3 !== 0); + const createdAt = new Date(Date.now() - (index + 2) * 2 * 60 * 60 * 1000); + return { + session_id: `mock-ai-${index + 1}`, + interaction_id: `mock-int-${index + 1}`, + thread_id: `thread-${index + 1}`, + channel, + queue_id: queue.queue_id, + status: handoff ? 'human_owned' : contained ? 'closed' : 'active', + reason_key: handoff ? reasonKey : '', + reason_label: handoff ? mockAiReasonLabel(reasonKey) : '', + raw_handoff_reason: handoff ? `raw:${reasonKey}` : '', + assigned_to: handoff ? (agents[index % Math.max(agents.length, 1)] || null) : null, + claimed_by_user: handoff ? `sup-${(index % 3) + 1}` : null, + assistant_turns: 6 + index, + user_turns: 3 + (index % 4), + tool_turns: 1 + (index % 2), + ai_latency_avg_ms: 1280 + index * 44, + ai_latency_p95_ms: 2010 + index * 61, + created_at: createdAt.toISOString(), + updated_at: new Date(createdAt.getTime() + 55 * 60 * 1000).toISOString(), + closed_at: contained || handoff ? new Date(createdAt.getTime() + 94 * 60 * 1000).toISOString() : null, + contained, + handoff, + human_touched: humanTouched, + closed_without_operator: closedWithoutOperator, + }; + }).filter((item) => { + if (normalized.channel && normalized.channel !== 'all' && item.channel !== normalized.channel) { + return false; + } + if (normalized.queue_id && item.queue_id !== normalized.queue_id) { + return false; + } + if (normalized.reason_key && item.reason_key !== normalized.reason_key) { + return false; + } + if (normalized.slice === 'contained' && !item.contained) { + return false; + } + if (normalized.slice === 'handoff' && !item.handoff) { + return false; + } + if (normalized.slice === 'human_touched' && !item.human_touched) { + return false; + } + if (normalized.slice === 'closed_without_operator' && !item.closed_without_operator) { + return false; + } + if (normalized.slice === 'active' && item.status !== 'active') { + return false; + } + if (normalized.status && item.status !== normalized.status) { + return false; + } + return true; + }); + return { + items: allItems.slice(offset, offset + limit), + total: allItems.length, + limit, + offset, + filters: normalized, + coverage: { + sessions_with_interaction_id: allItems.length, + sessions_with_queue_id: allItems.length, + sessions_with_latency_turns: allItems.length, + sessions_with_terminal_state: Math.round(allItems.length * 0.86), + sessions_with_handoff_reason: allItems.filter((item) => item.reason_key).length, + }, + }; +} + +function buildMockAiDetail(sessionId) { + const session = buildMockAiDrilldownData({ slice: 'all' }, 20, 0).items.find((item) => item.session_id === sessionId) + || buildMockAiDrilldownData({ slice: 'all' }, 1, 0).items[0]; + return { + session, + interaction: { + interaction_id: session.interaction_id, + channel: session.channel, + queue_id: session.queue_id, + status: session.handoff ? 'in_progress' : 'closed', + assigned_to: session.assigned_to, + subject: `Связанное обращение ${session.interaction_id}`, + updated_at: session.updated_at, + }, + timeline: [ + { label: 'Создана AI-сессия', event_type: 'session.created', ts: session.created_at, metadata: { channel: session.channel, queue_id: session.queue_id } }, + { label: 'Ответ AI', event_type: 'assistant.turn', ts: new Date(new Date(session.created_at).getTime() + 12 * 60 * 1000).toISOString(), metadata: { assistant_turns: session.assistant_turns } }, + { label: session.handoff ? 'Передано оператору' : 'Сессия закрыта', event_type: session.handoff ? 'session.handoff' : 'session.closed', ts: session.updated_at, metadata: { reason_key: session.reason_key || null, assigned_to: session.assigned_to || null } }, + ], + }; +} + +function buildMockAnalyticsDashboard(rangeMeta) { + const queueOptions = mockAnalyticsQueueOptions(); + const overview = buildMockAnalyticsOverview(rangeMeta); + return { + queueOptions, + overview, + compare: buildMockAnalyticsOverview(rangeMeta, { previous: true }), + coverage: { + implemented_metrics: Array.isArray(analyticsMockData().dashboard_coverage?.implemented_metrics) + ? analyticsMockData().dashboard_coverage.implemented_metrics + : [], + dimensions: Array.isArray(analyticsMockData().dashboard_coverage?.dimensions) + ? analyticsMockData().dashboard_coverage.dimensions + : [], + supported_filters: Array.isArray(analyticsMockData().dashboard_coverage?.supported_filters) + ? analyticsMockData().dashboard_coverage.supported_filters + : [], + }, + voiceNameOverview: buildMockVoiceNameOverview(rangeMeta), + voiceNameCompare: buildMockVoiceNameOverview(rangeMeta, { previous: true }), + aiOverview: buildMockAiOverview(rangeMeta), + aiCompare: buildMockAiOverview(rangeMeta, { previous: true }), + agentOverview: buildMockAgentOverview(rangeMeta), + agentCompare: buildMockAgentOverview(rangeMeta, { previous: true }), + trend: buildMockTrendPayloads(rangeMeta, state.analytics.trendMetric || 'volume'), + voiceNameTrend: buildMockVoiceNameTrend(rangeMeta, state.analytics.voiceNameTrendMetric || 'scenario_calls'), + aiTrend: buildMockAiTrend(rangeMeta, state.analytics.aiTrendMetric || 'containment_rate'), + agentTrend: buildMockAgentTrend(rangeMeta, state.analytics.agentTrendMetric || 'interactions_per_agent'), + queueRows: buildMockQueueRows(queueOptions, rangeMeta), + channelRows: Object.entries(overview.breakdowns.by_channel).map(([channel, payload]) => ({ + channel, + total: Number(payload?.total || 0), + answered: Number(payload?.answered || 0), + abandoned: Number(payload?.abandoned || 0), + })).sort((a, b) => b.total - a.total), + }; +} + +function applyAnalyticsSnapshot(snapshot = {}) { + state.analytics.preset = snapshot.preset || '7d'; + state.analytics.fromTs = snapshot.fromTs || ''; + state.analytics.toTs = snapshot.toTs || ''; + state.analytics.queueId = snapshot.queueId || 'all'; + state.analytics.channel = snapshot.channel || 'all'; + state.analytics.compareMode = snapshot.compareMode || 'previous'; + state.analytics.trendMetric = snapshot.trendMetric || 'volume'; + state.analytics.voiceNameTrendMetric = snapshot.voiceNameTrendMetric || 'scenario_calls'; + state.analytics.aiTrendMetric = snapshot.aiTrendMetric || 'containment_rate'; + state.analytics.agentTrendMetric = snapshot.agentTrendMetric || 'interactions_per_agent'; +} + +function withAnalyticsUrlSyncSuspended(callback) { + analyticsUrlSyncSuspended = true; + try { + return callback(); + } finally { + analyticsUrlSyncSuspended = false; + } +} + +function sanitizeAnalyticsSnapshot(snapshot = {}) { + const validChannel = ANALYTICS_CHANNEL_OPTIONS.some((item) => item.value === snapshot.channel); + return { + preset: Object.prototype.hasOwnProperty.call(ANALYTICS_PRESET_LABELS, snapshot.preset) ? snapshot.preset : '7d', + fromTs: snapshot.fromTs || '', + toTs: snapshot.toTs || '', + queueId: snapshot.queueId || 'all', + channel: validChannel ? snapshot.channel : 'all', + compareMode: snapshot.compareMode === 'off' ? 'off' : 'previous', + trendMetric: ANALYTICS_TREND_OPTIONS.includes(snapshot.trendMetric) ? snapshot.trendMetric : 'volume', + voiceNameTrendMetric: VOICE_NAME_TREND_OPTIONS.includes(snapshot.voiceNameTrendMetric) + ? snapshot.voiceNameTrendMetric + : 'scenario_calls', + aiTrendMetric: AI_ANALYTICS_TREND_OPTIONS.includes(snapshot.aiTrendMetric) + ? snapshot.aiTrendMetric + : 'containment_rate', + agentTrendMetric: AGENT_ANALYTICS_TREND_OPTIONS.includes(snapshot.agentTrendMetric) + ? snapshot.agentTrendMetric + : 'interactions_per_agent', + }; +} + +function analyticsDeepLinkDrilldownState() { + if (!state.drilldown.open || !state.drilldown.filters) { + return null; + } + return { + mode: state.drilldown.mode || 'interaction', + sourceType: state.drilldown.sourceType || '', + sourceValue: state.drilldown.sourceValue || '', + metric: state.drilldown.metric || '', + filters: normalizeAnalyticsDrilldownFilters(state.drilldown.filters), + offset: Number(state.drilldown.offset || 0), + selectedId: state.drilldown.selectedInteractionId || '', + }; +} + +function syncAnalyticsUrlState() { + if (analyticsUrlSyncSuspended) { + return; + } + const url = new URL(window.location.href); + ANALYTICS_DEEP_LINK_KEYS.forEach((key) => { + url.searchParams.delete(key); + }); + + const snapshot = sanitizeAnalyticsSnapshot(analyticsCurrentSnapshot()); + url.searchParams.set('preset', snapshot.preset); + if (snapshot.fromTs) { + url.searchParams.set('from', snapshot.fromTs); + } + if (snapshot.toTs) { + url.searchParams.set('to', snapshot.toTs); + } + if (snapshot.queueId && snapshot.queueId !== 'all') { + url.searchParams.set('queue', snapshot.queueId); + } + if (snapshot.channel && snapshot.channel !== 'all') { + url.searchParams.set('channel', snapshot.channel); + } + if (snapshot.compareMode !== 'previous') { + url.searchParams.set('compare', snapshot.compareMode); + } + if (snapshot.trendMetric !== 'volume') { + url.searchParams.set('trend', snapshot.trendMetric); + } + if (snapshot.voiceNameTrendMetric !== 'scenario_calls') { + url.searchParams.set('voice_name_trend', snapshot.voiceNameTrendMetric); + } + if (snapshot.aiTrendMetric !== 'containment_rate') { + url.searchParams.set('ai_trend', snapshot.aiTrendMetric); + } + if (snapshot.agentTrendMetric !== 'interactions_per_agent') { + url.searchParams.set('agent_trend', snapshot.agentTrendMetric); + } + if (state.analytics.activeViewId) { + url.searchParams.set('view', state.analytics.activeViewId); + } + + const drilldown = analyticsDeepLinkDrilldownState(); + if (drilldown) { + url.searchParams.set('dd', '1'); + url.searchParams.set('dd_mode', drilldown.mode); + url.searchParams.set('dd_source', drilldown.sourceType || 'overview'); + if (drilldown.sourceValue) { + url.searchParams.set('dd_value', drilldown.sourceValue); + } + if (drilldown.metric) { + url.searchParams.set('dd_metric', drilldown.metric); + } + if (drilldown.filters.slice && drilldown.filters.slice !== 'all') { + url.searchParams.set('dd_slice', drilldown.filters.slice); + } + if (drilldown.filters.reason_key) { + url.searchParams.set('dd_reason', drilldown.filters.reason_key); + } + if (drilldown.filters.status) { + url.searchParams.set('dd_status', drilldown.filters.status); + } + if (drilldown.filters.q) { + url.searchParams.set('dd_q', drilldown.filters.q); + } + if (drilldown.filters.sort_by && drilldown.filters.sort_by !== 'created_at') { + url.searchParams.set('dd_sort_by', drilldown.filters.sort_by); + } + if (drilldown.filters.sort_dir && drilldown.filters.sort_dir !== 'desc') { + url.searchParams.set('dd_sort_dir', drilldown.filters.sort_dir); + } + if (drilldown.filters.agent_id) { + url.searchParams.set('dd_agent', drilldown.filters.agent_id); + } + if (drilldown.offset > 0) { + url.searchParams.set('dd_offset', String(drilldown.offset)); + } + if (drilldown.selectedId) { + url.searchParams.set('dd_selected', drilldown.selectedId); + } + } + + const nextUrl = `${url.pathname}${url.search}${url.hash}`; + const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`; + if (nextUrl !== currentUrl) { + window.history.replaceState(null, '', nextUrl); + } +} + +function parseAnalyticsDeepLinkState() { + const params = new URLSearchParams(window.location.search); + const hasSnapshot = [ + 'preset', + 'from', + 'to', + 'queue', + 'channel', + 'compare', + 'trend', + 'voice_name_trend', + 'ai_trend', + 'agent_trend', + ].some((key) => params.has(key)); + const snapshot = sanitizeAnalyticsSnapshot({ + preset: params.get('preset') || '7d', + fromTs: params.get('from') || '', + toTs: params.get('to') || '', + queueId: params.get('queue') || 'all', + channel: params.get('channel') || 'all', + compareMode: params.get('compare') || 'previous', + trendMetric: params.get('trend') || 'volume', + voiceNameTrendMetric: params.get('voice_name_trend') || 'scenario_calls', + aiTrendMetric: params.get('ai_trend') || 'containment_rate', + agentTrendMetric: params.get('agent_trend') || 'interactions_per_agent', + }); + let drilldown = null; + if (params.get('dd') === '1') { + const mode = ['interaction', 'metric', 'ai'].includes(params.get('dd_mode')) + ? params.get('dd_mode') + : 'interaction'; + drilldown = { + mode, + sourceType: params.get('dd_source') || (mode === 'metric' ? 'metric' : mode === 'ai' ? 'ai-overview' : 'overview'), + sourceValue: params.get('dd_value') || '', + metric: params.get('dd_metric') || '', + offset: Math.max(0, Number(params.get('dd_offset') || 0)), + selectedId: params.get('dd_selected') || '', + filters: normalizeAnalyticsDrilldownFilters({ + metric: params.get('dd_metric') || '', + slice: params.get('dd_slice') || 'all', + reason_key: params.get('dd_reason') || '', + agent_id: params.get('dd_agent') || '', + status: params.get('dd_status') || '', + q: params.get('dd_q') || '', + sort_by: params.get('dd_sort_by') || 'created_at', + sort_dir: params.get('dd_sort_dir') || 'desc', + }), + }; + } + return { + hasSnapshot, + snapshot, + activeViewId: params.get('view') || '', + drilldown, + }; +} + +function restoreAnalyticsDrilldownFromUrlState(drilldownState) { + if (!drilldownState) { + return; + } + const mode = ['interaction', 'metric', 'ai'].includes(drilldownState.mode) ? drilldownState.mode : 'interaction'; + const sourceType = drilldownState.sourceType || (mode === 'metric' ? 'metric' : mode === 'ai' ? 'ai-overview' : 'overview'); + const sourceValue = drilldownState.sourceValue || ''; + const filters = { + ...analyticsDrilldownBaseFilters(state.analytics.lastRangeMeta || analyticsRangeFromControls()), + ...normalizeAnalyticsDrilldownFilters(drilldownState.filters || {}), + }; + if (mode === 'metric') { + filters.metric = drilldownState.metric || filters.metric || sourceValue; + filters.status = ''; + filters.q = ''; + } + if (mode === 'ai') { + filters.slice = filters.slice || 'all'; + if (sourceType === 'ai-overview' && sourceValue) { + filters.slice = sourceValue; + } + if (sourceType === 'ai-channel' && sourceValue) { + filters.channel = sourceValue; + } + if (sourceType === 'ai-reason' && sourceValue) { + filters.slice = 'handoff'; + filters.reason_key = sourceValue; + } + } + if (sourceType === 'channel' && sourceValue) { + filters.channel = sourceValue; + } + if (sourceType === 'queue' && sourceValue) { + filters.queue_id = sourceValue; + } + if (sourceType === 'agent' && sourceValue) { + filters.agent_id = sourceValue; + } + state.drilldown = { + ...emptyDrilldownState(), + open: true, + mode, + sourceType, + sourceLabel: analyticsDrilldownSourceLabel(sourceType, sourceValue), + sourceValue, + metric: mode === 'metric' ? (drilldownState.metric || filters.metric || sourceValue) : '', + filters: normalizeAnalyticsDrilldownFilters(filters), + offset: Math.max(0, Number(drilldownState.offset || 0)), + selectedInteractionId: drilldownState.selectedId || '', + coverage: mode === 'metric' ? analyticsMetricCoverage(drilldownState.metric || filters.metric || sourceValue) : null, + metricNote: mode === 'metric' + ? analyticsMetricDrilldownNote(drilldownState.metric || filters.metric || sourceValue) + : mode === 'ai' + ? aiAnalyticsDrilldownNote(filters) + : '', + }; + renderAnalyticsDrilldown(); + void loadAnalyticsDrilldownPage({ preserveSelection: Boolean(drilldownState.selectedId) }); +} + +function analyticsCompareModeLabel(mode) { + return ANALYTICS_COMPARE_MODE_LABELS[mode] || mode || 'Сравнение'; +} + +function suggestAnalyticsViewName() { + const parts = [analyticsPresetLabel(state.analytics.preset || '7d')]; + if (state.analytics.channel && state.analytics.channel !== 'all') { + parts.push(analyticsChannelLabel(state.analytics.channel)); + } + if (state.analytics.queueId && state.analytics.queueId !== 'all') { + const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId); + parts.push(queue?.name || state.analytics.queueId); + } + return parts.join(' · '); +} + +function normalizeSavedAnalyticsView(item = {}) { + return { + id: String(item.id || `view-${Date.now()}`), + name: String(item.name || 'Сохранённый вид'), + snapshot: { + preset: item.snapshot?.preset || '7d', + fromTs: item.snapshot?.fromTs || '', + toTs: item.snapshot?.toTs || '', + queueId: item.snapshot?.queueId || 'all', + channel: item.snapshot?.channel || 'all', + compareMode: item.snapshot?.compareMode || 'previous', + trendMetric: item.snapshot?.trendMetric || 'volume', + voiceNameTrendMetric: item.snapshot?.voiceNameTrendMetric || 'scenario_calls', + aiTrendMetric: item.snapshot?.aiTrendMetric || 'containment_rate', + agentTrendMetric: item.snapshot?.agentTrendMetric || 'interactions_per_agent', + }, + createdAt: item.created_at || item.createdAt || item.updated_at || item.updatedAt || new Date().toISOString(), + updatedAt: item.updated_at || item.updatedAt || item.created_at || item.createdAt || new Date().toISOString(), + }; +} + +async function loadSavedAnalyticsViews() { + try { + const items = await api('reporting', 'reports/views'); + state.analytics.savedViews = Array.isArray(items) + ? items + .filter((item) => item && typeof item === 'object') + .map((item) => normalizeSavedAnalyticsView(item)) + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + : []; + } catch (err) { + state.analytics.savedViews = []; + state.analytics.statusMessage = `Не удалось загрузить сохранённые виды: ${err.message}`; + } + if ( + state.analytics.activeViewId + && !state.analytics.savedViews.some((item) => item.id === state.analytics.activeViewId) + ) { + state.analytics.activeViewId = ''; + } + renderSavedAnalyticsViews(); +} + +function renderSavedAnalyticsViews() { + const select = $('analyticsSavedViewSelect'); + const input = $('analyticsSavedViewName'); + const meta = $('analyticsSavedViewMeta'); + const deleteBtn = $('analyticsDeleteViewBtn'); + if (!select || !input || !meta || !deleteBtn) { + return; + } + + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null; + select.innerHTML = [ + '', + ...state.analytics.savedViews.map( + (item) => ``, + ), + ].join(''); + select.value = activeView ? activeView.id : ''; + + if (activeView) { + input.value = activeView.name; + } + input.placeholder = suggestAnalyticsViewName(); + deleteBtn.disabled = !activeView; + + const parts = []; + parts.push( + state.analytics.savedViews.length + ? `${state.analytics.savedViews.length} сохранённых ${state.analytics.savedViews.length === 1 ? 'вид' : 'вида'}` + : 'Сохранённых видов пока нет', + ); + if (activeView) { + parts.push(`активен: ${activeView.name}`); + } + if (state.analytics.statusMessage) { + parts.push(state.analytics.statusMessage); + } + meta.textContent = parts.join(' · '); +} + +async function saveAnalyticsView() { + syncAnalyticsStateFromControls(); + const input = $('analyticsSavedViewName'); + const name = input?.value.trim() || suggestAnalyticsViewName(); + try { + const view = await api('reporting', 'reports/views', { + method: 'POST', + body: JSON.stringify({ + id: state.analytics.activeViewId || null, + name, + snapshot: analyticsCurrentSnapshot(), + }), + }); + const normalized = normalizeSavedAnalyticsView(view); + const existingIndex = state.analytics.savedViews.findIndex((item) => item.id === normalized.id); + if (existingIndex >= 0) { + state.analytics.savedViews.splice(existingIndex, 1, normalized); + state.analytics.statusMessage = `Вид «${name}» обновлён.`; + } else { + state.analytics.savedViews.unshift(normalized); + state.analytics.statusMessage = `Вид «${name}» сохранён.`; + } + state.analytics.savedViews = state.analytics.savedViews + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + .slice(0, 12); + state.analytics.activeViewId = normalized.id; + } catch (err) { + state.analytics.statusMessage = `Не удалось сохранить вид: ${err.message}`; + } + renderSavedAnalyticsViews(); +} + +async function deleteAnalyticsView() { + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null; + if (!activeView) { + state.analytics.statusMessage = 'Сначала выберите вид, который нужно удалить.'; + renderSavedAnalyticsViews(); + return; + } + try { + await api('reporting', `reports/views/${encodeURIComponent(activeView.id)}`, { method: 'DELETE' }); + state.analytics.savedViews = state.analytics.savedViews.filter((item) => item.id !== activeView.id); + state.analytics.activeViewId = ''; + state.analytics.statusMessage = `Вид «${activeView.name}» удалён.`; + } catch (err) { + state.analytics.statusMessage = `Не удалось удалить вид: ${err.message}`; + } + renderSavedAnalyticsViews(); +} + +function detachActiveAnalyticsView() { + if (!state.analytics.activeViewId) { + return; + } + state.analytics.activeViewId = ''; + state.analytics.statusMessage = 'Фильтры изменены. Можно сохранить новый вид.'; +} + +function analyticsQuery(range, overrides = {}) { + const params = new URLSearchParams(); + params.set('from_ts', range.from.toISOString()); + params.set('to_ts', range.to.toISOString()); + + const queueId = overrides.queueId ?? state.analytics.queueId; + const channel = overrides.channel ?? state.analytics.channel; + + if (queueId && queueId !== 'all') { + params.set('queue_id', queueId); + } + if (channel && channel !== 'all') { + params.set('channel', channel); + } + + return params.toString(); +} + +async function fetchAnalyticsKpi(range, overrides = {}) { + return api('reporting', `reports/kpi?${analyticsQuery(range, overrides)}`); +} + +async function fetchAgentAnalyticsOverview(range, overrides = {}) { + return api('reporting', `reports/agents/overview?${analyticsQuery(range, overrides)}`); +} + +async function fetchAgentAnalyticsTimeseries(range, metric, interval, overrides = {}) { + const params = new URLSearchParams(analyticsQuery(range, overrides)); + params.set('metric', metric || state.analytics.agentTrendMetric || 'interactions_per_agent'); + params.set('interval', interval || 'day'); + return api('reporting', `reports/agents/timeseries?${params.toString()}`); +} + +function voiceNameAnalyticsSupportedChannel(channel = state.analytics.channel) { + return channel === 'all' || channel === 'voice'; +} + +function voiceNameAnalyticsQuery(range, overrides = {}) { + const params = new URLSearchParams(); + params.set('from_ts', range.from.toISOString()); + params.set('to_ts', range.to.toISOString()); + const queueId = overrides.queueId ?? state.analytics.queueId; + const language = overrides.language ?? null; + if (queueId && queueId !== 'all') { + params.set('queue_id', queueId); + } + if (language) { + params.set('language', language); + } + return params.toString(); +} + +async function fetchVoiceNameAnalyticsOverview(range, overrides = {}) { + return api('ai', `ai/analytics/voice-name-flow/overview?${voiceNameAnalyticsQuery(range, overrides)}`); +} + +async function fetchVoiceNameAnalyticsTimeseries(range, metric, overrides = {}) { + const params = new URLSearchParams(voiceNameAnalyticsQuery(range, overrides)); + params.set('metric', metric || state.analytics.voiceNameTrendMetric || 'scenario_calls'); + return api('ai', `ai/analytics/voice-name-flow/timeseries?${params.toString()}`); +} + +async function fetchAnalyticsMetricDrilldown(filters, limit, offset) { + return api('reporting', `reports/drilldown?${analyticsMetricDrilldownQuery(filters, limit, offset)}`); +} + +function aiAnalyticsSupportedChannel(channel = state.analytics.channel) { + return channel === 'all' || channel === 'telegram' || channel === 'whatsapp'; +} + +function aiAnalyticsQuery(range, overrides = {}) { + const params = new URLSearchParams(); + params.set('from_ts', range.from.toISOString()); + params.set('to_ts', range.to.toISOString()); + + const queueId = overrides.queueId ?? state.analytics.queueId; + const channel = overrides.channel ?? state.analytics.channel; + + if (queueId && queueId !== 'all') { + params.set('queue_id', queueId); + } + if (channel) { + params.set('channel', channel); + } + return params.toString(); +} + +async function fetchAiAnalyticsOverview(range, overrides = {}) { + return api('ai', `ai/analytics/overview?${aiAnalyticsQuery(range, overrides)}`); +} + +async function fetchAiAnalyticsTimeseries(range, metric, interval, overrides = {}) { + const params = new URLSearchParams(aiAnalyticsQuery(range, overrides)); + params.set('metric', metric || state.analytics.aiTrendMetric || 'containment_rate'); + params.set('interval', interval || 'day'); + return api('ai', `ai/analytics/timeseries?${params.toString()}`); +} + +async function fetchAiAnalyticsDrilldown(filters, limit, offset) { + return api('ai', `ai/analytics/drilldown?${aiAnalyticsDrilldownQuery(filters, limit, offset)}`); +} + +async function fetchAiAnalyticsSessionDetail(sessionId) { + return api('ai', `ai/analytics/sessions/${encodeURIComponent(sessionId)}`); +} + +function renderAnalyticsQueueOptions() { + const select = $('analyticsQueue'); + if (!select) { + return; + } + const selected = state.analytics.queueId || 'all'; + const options = ['']; + if (selected !== 'all' && !state.analytics.queueOptions.some((item) => item.queue_id === selected)) { + options.push(``); + } + options.push( + ...state.analytics.queueOptions.map( + (item) => ``, + ), + ); + select.innerHTML = options.join(''); + select.value = options.some((item) => item.includes(`value="${selected}"`)) ? selected : 'all'; +} + +async function loadAnalyticsQueueOptions() { + try { + const data = await api('routing', 'queues'); + state.analytics.queueOptions = Array.isArray(data) ? data : []; + state.analytics.queueAccessError = ''; + } catch (err) { + state.analytics.queueOptions = []; + state.analytics.queueAccessError = err.message; + } + renderAnalyticsQueueOptions(); + return state.analytics.queueOptions; +} + +function formatAnalyticsNumber(value, digits = 0) { + return new Intl.NumberFormat('ru-RU', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }).format(Number(value || 0)); +} + +function analyticsChannelLabel(channel) { + const meta = ANALYTICS_CHANNEL_OPTIONS.find((item) => item.value === channel); + return meta ? meta.label : String(channel || 'неизвестно'); +} + +function metricValueFromPayload(metric, payload) { + if (metric === 'volume' || metric === 'total') { + return Number(payload?.volume?.total || 0); + } + if (metric === 'answered') { + return Number(payload?.volume?.answered || 0); + } + return Number(payload?.kpi?.[metric] || 0); +} + +function formatAnalyticsMetric(metric, value) { + const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count' }; + if (meta.unit === 'pct') { + return `${formatAnalyticsNumber(value, 2)}%`; + } + if (meta.unit === 'seconds') { + return `${formatAnalyticsNumber(value, 2)} с`; + } + return formatAnalyticsNumber(value, 0); +} + +function analyticsMetricCoverage(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const detail = payload?.coverage?.metric_details?.[metric]; + if (detail && typeof detail === 'object') { + return { + ...emptyAnalyticsMetricCoverage(), + ...detail, + supported_channels: Array.isArray(detail.supported_channels) ? detail.supported_channels : [], + exact_rows: Number(detail.exact_rows || 0), + total_rows: Number(detail.total_rows || 0), + }; + } + return { + ...emptyAnalyticsMetricCoverage(), + status: payload?.coverage?.metric_status?.[metric] || 'unavailable', + supported_channels: Array.isArray(payload?.coverage?.supported_channels?.[metric]) + ? payload.coverage.supported_channels[metric] + : [], + }; +} + +function analyticsMetricCanDrilldown(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + return analyticsMetricCoverage(metric, payload).status === 'exact'; +} + +function analyticsMetricCoverageTone(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const coverage = analyticsMetricCoverage(metric, payload); + if (coverage.status === 'exact') { + return 'exact'; + } + if (coverage.status === 'partial') { + return 'partial'; + } + return 'unavailable'; +} + +function analyticsMetricCoverageLabel(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const coverage = analyticsMetricCoverage(metric, payload); + if (coverage.status === 'exact') { + return 'Точно'; + } + if ( + coverage.status === 'partial' + && coverage.supported_channels.length === 1 + && coverage.supported_channels[0] === 'voice' + ) { + return 'Только голос'; + } + if (coverage.status === 'partial') { + return 'Частично'; + } + return 'Нет точного покрытия'; +} + +function analyticsMetricCoverageHint(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const coverage = analyticsMetricCoverage(metric, payload); + if (coverage.status === 'exact') { + return 'Открыть точный KPI-срез по ID обращения.'; + } + if ( + coverage.supported_channels.length === 1 + && coverage.supported_channels[0] === 'voice' + && coverage.status === 'partial' + ) { + return 'Точная детализация сейчас доступна только для голосовой части выбранного окна.'; + } + if (!coverage.total_rows) { + return 'Для выбранного окна пока нет точных записей фактов для этой метрики.'; + } + if (!coverage.supported_channels.length) { + return 'Для этой метрики точная детализация в текущем срезе пока недоступна.'; + } + return 'Точная детализация пока доступна только для части среза, покрытой слоем KPI-фактов.'; +} + +function analyticsMetricCoverageBadge(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const tone = analyticsMetricCoverageTone(metric, payload); + const label = analyticsMetricCoverageLabel(metric, payload); + return `${escapeHtml(label)}`; +} + +function aiAnalyticsMetricValue(metric, payload) { + return payload?.metrics?.[metric] ?? null; +} + +function aiAnalyticsMetricSlice(metric) { + if (metric === 'containment_rate') { + return 'contained'; + } + if (metric === 'handoff_rate') { + return 'handoff'; + } + if (metric === 'human_touched_rate') { + return 'human_touched'; + } + if (metric === 'closed_without_operator_rate') { + return 'closed_without_operator'; + } + return 'all'; +} + +function aiAnalyticsStatusLabel(status) { + const labels = { + active: 'Активна', + closed: 'Закрыта', + handoff_required: 'Требуется передача', + human_owned: 'У оператора', + error: 'Ошибка', + }; + return labels[status] || status || 'Неизвестно'; +} + +function aiAnalyticsSliceLabel(slice) { + const labels = { + all: 'Все AI-сессии', + contained: 'Закрыто AI', + handoff: 'Передано оператору', + human_touched: 'С участием оператора', + closed_without_operator: 'Закрыто без оператора', + active: 'Активные', + error: 'С ошибкой', + }; + return labels[slice] || slice || 'AI-срез'; +} + +function aiAnalyticsSortLabel(filters = state.drilldown.filters) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const fieldLabels = { + created_at: 'созданию', + updated_at: 'обновлению', + ai_latency_avg_ms: 'задержке', + status: 'статусу', + }; + const dirLabel = normalized.sort_dir === 'asc' ? 'по возрастанию' : 'по убыванию'; + return `Сортировка: по ${fieldLabels[normalized.sort_by] || normalized.sort_by}, ${dirLabel}`; +} + +function aiAnalyticsDrilldownNote(filters = state.drilldown.filters) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const parts = [ + 'Только метаданные по AI-сессиям: без текстов сообщений, AI-сводок, записей и транскриптов.', + ]; + if (normalized.reason_key) { + parts.push(`Причина: ${normalized.reason_key}`); + } + return parts.join(' '); +} + +function formatAiAnalyticsMetric(metric, value) { + const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count' }; + if (value === null || value === undefined || Number.isNaN(Number(value))) { + return '—'; + } + if (meta.unit === 'pct') { + return `${formatAnalyticsNumber(value, 2)}%`; + } + if (meta.unit === 'ms') { + return `${formatAnalyticsNumber(value, 0)} ms`; + } + return formatAnalyticsNumber(value, 0); +} + +function signedDelta(value, digits = 1) { + if (!value) { + return '0'; + } + const sign = value > 0 ? '+' : '-'; + return `${sign}${formatAnalyticsNumber(Math.abs(value), digits)}`; +} + +function analyticsDelta(metric, currentPayload, previousPayload) { + const current = metricValueFromPayload(metric, currentPayload); + const previous = metricValueFromPayload(metric, previousPayload); + const diff = current - previous; + const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count', better: 'neutral' }; + + let text = signedDelta(diff, meta.unit === 'count' ? 0 : 1); + if (meta.unit === 'pct') { + text = `${text} п.п.`; + } else if (meta.unit === 'seconds') { + text = `${text} с`; + } + + let tone = 'neutral'; + if (meta.better === 'up') { + tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral'; + } else if (meta.better === 'down') { + tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral'; + } + + return { text, tone }; +} + +function voiceNameAnalyticsMetricLabel(metric) { + return VOICE_NAME_TREND_LABELS[metric] || metric; +} + +function voiceNameAnalyticsMetricValue(metric, payload = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview()) { + if (metric === 'scenario_calls') { + return Number(payload?.totals?.scenario_calls || 0); + } + return Number(payload?.metrics?.[metric] || 0); +} + +function formatVoiceNameAnalyticsMetric(metric, value) { + const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count' }; + if (value === null || value === undefined || Number.isNaN(Number(value))) { + return '—'; + } + if (meta.unit === 'pct') { + return `${formatAnalyticsNumber(value, 2)}%`; + } + return formatAnalyticsNumber(value, 0); +} + +function formatVoiceNameAnalyticsAxisValue(metric, value) { + const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count' }; + return formatAnalyticsNumber(value, meta.unit === 'pct' ? 1 : 0); +} + +function voiceNameAnalyticsDelta(metric, currentPayload, previousPayload) { + const current = voiceNameAnalyticsMetricValue(metric, currentPayload); + const previous = voiceNameAnalyticsMetricValue(metric, previousPayload); + const diff = Number(current || 0) - Number(previous || 0); + const meta = VOICE_NAME_METRIC_META[metric] || { unit: 'count', better: 'neutral' }; + let text = signedDelta(diff, meta.unit === 'pct' ? 1 : 0); + if (meta.unit === 'pct') { + text = `${text} п.п.`; + } + let tone = 'neutral'; + if (meta.better === 'up') { + tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral'; + } else if (meta.better === 'down') { + tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral'; + } + return { text, tone }; +} + +function aiAnalyticsMetricLabel(metric) { + return AI_ANALYTICS_TREND_LABELS[metric] || metric; +} + +function aiAnalyticsDelta(metric, currentPayload, previousPayload) { + const current = aiAnalyticsMetricValue(metric, currentPayload); + const previous = aiAnalyticsMetricValue(metric, previousPayload); + const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count', better: 'neutral' }; + if ( + current === null + || current === undefined + || previous === null + || previous === undefined + || Number.isNaN(Number(current)) + || Number.isNaN(Number(previous)) + ) { + return { text: '-', tone: 'neutral' }; + } + + const diff = Number(current) - Number(previous); + let text = signedDelta(diff, meta.unit === 'pct' ? 1 : 0); + if (meta.unit === 'pct') { + text = `${text} п.п.`; + } else if (meta.unit === 'ms') { + text = `${text} ms`; + } + + let tone = 'neutral'; + if (meta.better === 'up') { + tone = diff > 0 ? 'positive' : diff < 0 ? 'negative' : 'neutral'; + } else if (meta.better === 'down') { + tone = diff < 0 ? 'positive' : diff > 0 ? 'negative' : 'neutral'; + } + return { text, tone }; +} + +function renderAnalyticsCard(label, metric, payload, previousPayload, options = {}) { + const value = metricValueFromPayload(metric, payload); + const showCompare = state.analytics.compareMode === 'previous'; + const delta = showCompare ? analyticsDelta(metric, payload, previousPayload) : null; + const note = ANALYTICS_METRIC_META[metric]?.note || ''; + const metricDrilldown = Boolean(options.metricDrilldown); + const drilldownEnabled = metricDrilldown + ? analyticsMetricCanDrilldown(metric, payload) + : Boolean(options.drilldown); + const coverageBadge = metricDrilldown ? analyticsMetricCoverageBadge(metric, payload) : ''; + const classes = [ + 'summary-card', + 'analytics-card', + drilldownEnabled ? 'analytics-card-button' : 'analytics-card-readonly', + metricDrilldown ? `analytics-card-coverage-${analyticsMetricCoverageTone(metric, payload)}` : '', + ].join(' '); + const attributes = drilldownEnabled + ? `type="button" data-analytics-drilldown-source="${escapeHtml(options.sourceType || 'overview')}" data-analytics-drilldown-label="${escapeHtml(options.sourceLabel || label)}"${metricDrilldown ? ` data-analytics-drilldown-metric="${escapeHtml(metric)}"` : ''}` + : ''; + const hint = metricDrilldown + ? analyticsMetricCoverageHint(metric, payload) + : drilldownEnabled + ? 'Открыть детализацию по обращениям' + : 'Точная детализация появится после связи KPI с interaction_id.'; + const tag = drilldownEnabled ? 'button' : 'div'; + return ` + <${tag} class="${classes}" ${attributes}> +
            +
            ${label}
            +
            + ${coverageBadge} + ${showCompare ? `${delta.text}` : ''} +
            +
            +
            ${formatAnalyticsMetric(metric, value)}
            + ${note ? `
            ${note}
            ` : ''} +
            ${hint}
            + + `; +} + +function formatTime(ts) { + if (!ts) { + return '-'; + } + const date = new Date(ts); + if (Number.isNaN(date.getTime())) { + return ts; + } + return date.toLocaleString(); +} + +function analyticsPresetLabel(preset) { + return ANALYTICS_PRESET_LABELS[preset] || preset; +} + +function analyticsMetricLabel(metric) { + return ANALYTICS_TREND_LABELS[metric] || metric; +} + +function analyticsQueueName(queueId) { + if (queueId === 'all') { + return 'Все очереди'; + } + if (!queueId) { + return 'Без очереди'; + } + const queue = state.analytics.queueOptions.find((item) => item.queue_id === queueId); + return queue?.name || queueId; +} + +function interactionStatusLabel(status) { + const labels = { + new: 'Новая', + in_progress: 'В работе', + escalated: 'Эскалация', + closed: 'Закрыта', + abandoned: 'Потеряна', + }; + return labels[status] || status || 'Не указан'; +} + +function analyticsMetricDrilldownNote(metric, payload = state.analytics.overview || emptyKpiEnvelope()) { + const metricNote = ANALYTICS_METRIC_META[metric]?.note || ''; + const coverageHint = analyticsMetricCoverageHint(metric, payload); + return [metricNote, coverageHint] + .filter(Boolean) + .join(' '); +} + +function analyticsDrilldownFactBadges(item) { + if (!item || typeof item !== 'object') { + return []; + } + const badges = []; + if (item.wait_seconds !== null && item.wait_seconds !== undefined) { + badges.push({ tone: 'wait', label: `Ожидание ${formatAnalyticsNumber(item.wait_seconds, 0)} с` }); + } + if (item.handle_seconds !== null && item.handle_seconds !== undefined) { + badges.push({ tone: 'handle', label: `Обработка ${formatAnalyticsNumber(item.handle_seconds, 0)} с` }); + } + if (item.within_sla === true) { + badges.push({ tone: 'sla', label: 'В SLA' }); + } else if (item.within_sla === false) { + badges.push({ tone: 'sla-miss', label: 'Вне SLA' }); + } + if (item.resolved_first_contact === true) { + badges.push({ tone: 'fcr', label: 'FCR' }); + } + if (item.abandoned === true) { + badges.push({ tone: 'abandoned', label: 'Потеря' }); + } + return badges; +} + +function renderAnalyticsDrilldownFactBadges(item) { + const badges = analyticsDrilldownFactBadges(item); + if (!badges.length) { + return ''; + } + return ` +
            + ${badges + .map( + (badge) => `${escapeHtml(badge.label)}`, + ) + .join('')} +
            + `; +} + +function clearAnalyticsDrilldownSearchDebounce() { + if (analyticsDrilldownSearchDebounce) { + window.clearTimeout(analyticsDrilldownSearchDebounce); + analyticsDrilldownSearchDebounce = 0; + } +} + +function normalizeAnalyticsDrilldownFilters(filters = {}) { + return { + from_ts: filters.from_ts || '', + to_ts: filters.to_ts || '', + metric: filters.metric || '', + slice: filters.slice || 'all', + reason_key: filters.reason_key || '', + queue_id: filters.queue_id || '', + channel: filters.channel || '', + agent_id: filters.agent_id || '', + sl_threshold_seconds: Number(filters.sl_threshold_seconds || 30), + status: filters.status || '', + q: filters.q || '', + sort_by: filters.sort_by || 'created_at', + sort_dir: filters.sort_dir || 'desc', + }; +} + +function analyticsDrilldownSortValue(filters = state.drilldown.filters) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + return `${normalized.sort_by}:${normalized.sort_dir}`; +} + +function parseAnalyticsDrilldownSort(value) { + const [sortByRaw = 'created_at', sortDirRaw = 'desc'] = String(value || 'created_at:desc').split(':'); + const allowedFields = state.drilldown.mode === 'ai' + ? ['created_at', 'updated_at', 'ai_latency_avg_ms', 'status'] + : ['created_at', 'updated_at', 'subject']; + const sortBy = allowedFields.includes(sortByRaw) ? sortByRaw : 'created_at'; + const sortDir = sortDirRaw === 'asc' ? 'asc' : 'desc'; + return { sort_by: sortBy, sort_dir: sortDir }; +} + +function analyticsDrilldownSortLabel(filters = state.drilldown.filters) { + if (state.drilldown.mode === 'ai') { + return aiAnalyticsSortLabel(filters); + } + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const fieldLabels = { + created_at: 'созданию', + updated_at: 'обновлению', + subject: 'теме', + }; + const dirLabel = normalized.sort_dir === 'asc' ? 'по возрастанию' : 'по убыванию'; + return `Сортировка: по ${fieldLabels[normalized.sort_by] || normalized.sort_by}, ${dirLabel}`; +} + +function analyticsDrilldownStatusClass(status) { + return String(status || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '-'); +} + +function analyticsDrilldownFilenameSource() { + if (state.drilldown.mode === 'ai') { + const slice = state.drilldown.filters?.slice || 'all'; + if (state.drilldown.filters?.reason_key) { + return `ai-${slice}-${String(state.drilldown.filters.reason_key).toLowerCase()}`; + } + if (state.drilldown.filters?.channel) { + return `ai-${String(state.drilldown.filters.channel).toLowerCase()}-${slice}`; + } + return `ai-${slice}`; + } + if (state.drilldown.sourceType === 'metric' && state.drilldown.metric) { + return `metric-${String(state.drilldown.metric).toLowerCase()}`; + } + if (state.drilldown.sourceType === 'channel' && state.drilldown.filters?.channel) { + return `channel-${String(state.drilldown.filters.channel).toLowerCase()}`; + } + if (state.drilldown.sourceType === 'queue' && state.drilldown.filters?.queue_id) { + return `queue-${String(state.drilldown.filters.queue_id).toLowerCase().replace(/[^a-z0-9_-]+/g, '-')}`; + } + if (state.drilldown.sourceType === 'agent' && state.drilldown.filters?.agent_id) { + return `agent-${String(state.drilldown.filters.agent_id).toLowerCase().replace(/[^a-z0-9._-]+/g, '-')}`; + } + return state.drilldown.sourceType === 'overview' ? 'all-interactions' : String(state.drilldown.sourceType || 'slice'); +} + +function sanitizeAnalyticsTimelineMetadata(metadata) { + if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) { + return {}; + } + const blockedKeyParts = ['message', 'messages', 'text', 'body', 'summary', 'transcript', 'recording', 'audio', 'media', 'content']; + const safe = {}; + Object.entries(metadata).forEach(([key, value]) => { + const normalizedKey = String(key || '').toLowerCase(); + if (blockedKeyParts.some((part) => normalizedKey.includes(part))) { + return; + } + if (typeof value === 'string') { + safe[key] = value.length > 120 ? `[hidden string ${value.length} chars]` : value; + return; + } + if (Array.isArray(value)) { + safe[key] = `[items: ${value.length}]`; + return; + } + if (value && typeof value === 'object') { + safe[key] = '[object]'; + return; + } + safe[key] = value; + }); + return safe; +} + +function rememberAnalyticsDrilldownReturnFocus() { + const active = document.activeElement; + analyticsDrilldownReturnFocus = active instanceof HTMLElement ? active : null; +} + +function restoreAnalyticsDrilldownReturnFocus() { + if (analyticsDrilldownReturnFocus instanceof HTMLElement && document.contains(analyticsDrilldownReturnFocus)) { + analyticsDrilldownReturnFocus.focus(); + } + analyticsDrilldownReturnFocus = null; +} + +function analyticsDrilldownFocusableElements() { + const drawer = $('analyticsDrilldownDrawer'); + if (!drawer || !state.drilldown.open) { + return []; + } + return Array.from( + drawer.querySelectorAll( + 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ).filter( + (element) => + !element.hasAttribute('hidden') + && element.getAttribute('aria-hidden') !== 'true' + && element.offsetParent !== null, + ); +} + +function focusAnalyticsDrilldownPrimaryControl(selectText = false) { + const searchInput = $('analyticsDrilldownSearch'); + if (searchInput && !searchInput.disabled) { + searchInput.focus(); + if (selectText && typeof searchInput.select === 'function') { + searchInput.select(); + } + return; + } + $('analyticsDrilldownDrawer')?.focus(); +} + +function handleAnalyticsDrilldownWindowKeydown(event) { + if (!state.drilldown.open) { + return; + } + if (event.key === 'Escape') { + event.preventDefault(); + closeAnalyticsDrilldown(); + return; + } + if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) { + const targetTag = document.activeElement?.tagName || ''; + if (!['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTag)) { + event.preventDefault(); + focusAnalyticsDrilldownPrimaryControl(true); + } + return; + } + if (event.key !== 'Tab') { + return; + } + const focusable = analyticsDrilldownFocusableElements(); + if (!focusable.length) { + return; + } + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + const active = document.activeElement; + if (event.shiftKey && active === first) { + event.preventDefault(); + last.focus(); + return; + } + if (!event.shiftKey && active === last) { + event.preventDefault(); + first.focus(); + } +} + +function maybeScrollAnalyticsDrilldownDetailIntoView() { + if (!window.matchMedia('(max-width: 960px)').matches) { + return; + } + window.requestAnimationFrame(() => { + $('analyticsDrilldownDetail')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); +} + +function syncAnalyticsDrilldownControls() { + const searchInput = $('analyticsDrilldownSearch'); + const statusSelect = $('analyticsDrilldownStatus'); + const sortSelect = $('analyticsDrilldownSort'); + const clearBtn = $('analyticsDrilldownClearBtn'); + const exportBtn = $('analyticsDrilldownExportBtn'); + const isOpen = state.drilldown.open; + const localFiltersEnabled = isOpen && state.drilldown.mode === 'interaction'; + const filters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters || {}); + if (searchInput) { + searchInput.value = isOpen ? filters.q : ''; + searchInput.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + searchInput.placeholder = state.drilldown.mode === 'metric' + ? 'Поиск доступен только для среза обращений' + : 'ID, тема или назначенный'; + } + if (statusSelect) { + statusSelect.value = isOpen ? (filters.status || 'all') : 'all'; + statusSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (sortSelect) { + sortSelect.value = analyticsDrilldownSortValue(filters); + sortSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (clearBtn) { + clearBtn.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (exportBtn) { + exportBtn.disabled = !isOpen || state.drilldown.loading || state.drilldown.exporting || !state.drilldown.total; + exportBtn.textContent = state.drilldown.exporting ? 'Готовим CSV...' : 'Экспорт CSV'; + } +} + +function resetAnalyticsDrilldown() { + clearAnalyticsDrilldownSearchDebounce(); + state.drilldown = emptyDrilldownState(); +} + +function closeAnalyticsDrilldown(options = {}) { + if (!state.drilldown.open && !state.drilldown.loading && !state.drilldown.error) { + return; + } + resetAnalyticsDrilldown(); + renderAnalyticsDrilldown(); + if (!options.skipUrlSync) { + syncAnalyticsUrlState(); + } + window.requestAnimationFrame(() => { + restoreAnalyticsDrilldownReturnFocus(); + }); +} + +function analyticsDrilldownBaseFilters(rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls()) { + return normalizeAnalyticsDrilldownFilters({ + from_ts: rangeMeta.current.from.toISOString(), + to_ts: rangeMeta.current.to.toISOString(), + metric: '', + slice: 'all', + reason_key: '', + queue_id: state.analytics.queueId !== 'all' ? state.analytics.queueId : '', + channel: state.analytics.channel !== 'all' ? state.analytics.channel : '', + agent_id: '', + sl_threshold_seconds: 30, + status: '', + q: '', + sort_by: 'created_at', + sort_dir: 'desc', + }); +} + +function analyticsDrilldownSourceLabel(sourceType, sourceValue = '') { + if (sourceType === 'agent') { + return `Агент: ${sourceValue}`; + } + if (sourceType === 'ai-overview') { + return aiAnalyticsSliceLabel(sourceValue || 'all'); + } + if (sourceType === 'ai-channel') { + return `AI-канал: ${analyticsChannelLabel(sourceValue)}`; + } + if (sourceType === 'ai-reason') { + return `Причина передачи: ${sourceValue}`; + } + if (sourceType === 'metric') { + return `KPI: ${sourceValue}`; + } + if (sourceType === 'channel') { + return `Канал: ${analyticsChannelLabel(sourceValue)}`; + } + if (sourceType === 'queue') { + return `Очередь: ${analyticsQueueName(sourceValue)}`; + } + return 'Все обращения'; +} + +function analyticsDrilldownQuery(filters, limit, offset) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const params = new URLSearchParams(); + params.set('from_ts', normalized.from_ts); + params.set('to_ts', normalized.to_ts); + params.set('limit', String(limit)); + params.set('offset', String(offset)); + params.set('sort_by', normalized.sort_by || 'created_at'); + params.set('sort_dir', normalized.sort_dir || 'desc'); + if (normalized.queue_id) { + params.set('queue_id', normalized.queue_id); + } + if (normalized.channel) { + params.set('channel', normalized.channel); + } + if (normalized.agent_id) { + params.set('agent_id', normalized.agent_id); + } + if (normalized.status) { + params.set('status', normalized.status); + } + if (normalized.q) { + params.set('q', normalized.q); + } + return params.toString(); +} + +function aiAnalyticsDrilldownQuery(filters, limit, offset) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const params = new URLSearchParams(); + params.set('from_ts', normalized.from_ts); + params.set('to_ts', normalized.to_ts); + params.set('slice', normalized.slice || 'all'); + params.set('limit', String(limit)); + params.set('offset', String(offset)); + params.set('sort_by', normalized.sort_by || 'created_at'); + params.set('sort_dir', normalized.sort_dir || 'desc'); + if (normalized.queue_id) { + params.set('queue_id', normalized.queue_id); + } + if (normalized.channel) { + params.set('channel', normalized.channel); + } + if (normalized.reason_key) { + params.set('reason_key', normalized.reason_key); + } + if (normalized.status) { + params.set('status', normalized.status); + } + if (normalized.q) { + params.set('q', normalized.q); + } + return params.toString(); +} + +function analyticsMetricDrilldownQuery(filters, limit, offset) { + const normalized = normalizeAnalyticsDrilldownFilters(filters); + const params = new URLSearchParams(); + params.set('from_ts', normalized.from_ts); + params.set('to_ts', normalized.to_ts); + params.set('metric', normalized.metric); + params.set('limit', String(limit)); + params.set('offset', String(offset)); + params.set('sl_threshold_seconds', String(normalized.sl_threshold_seconds || 30)); + if (normalized.queue_id) { + params.set('queue_id', normalized.queue_id); + } + if (normalized.channel) { + params.set('channel', normalized.channel); + } + return params.toString(); +} + +function analyticsDrilldownChips() { + if (!state.drilldown.filters) { + return []; + } + const drilldownFilters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters); + const chips = []; + chips.push(state.drilldown.sourceLabel || 'Детализация'); + chips.push( + `${formatTime(drilldownFilters.from_ts)} - ${formatTime(drilldownFilters.to_ts)}`, + ); + if (drilldownFilters.channel) { + chips.push(analyticsChannelLabel(drilldownFilters.channel)); + } + if (drilldownFilters.queue_id) { + chips.push(analyticsQueueName(drilldownFilters.queue_id)); + } + if (drilldownFilters.agent_id) { + chips.push(`Агент: ${drilldownFilters.agent_id}`); + } + if (drilldownFilters.status) { + chips.push(`Статус: ${interactionStatusLabel(drilldownFilters.status)}`); + } + if (drilldownFilters.q) { + chips.push(`Поиск: ${drilldownFilters.q}`); + } + if (state.drilldown.mode === 'metric' && state.drilldown.metric) { + chips.push(analyticsMetricCoverageLabel(state.drilldown.metric)); + } else { + chips.push(analyticsDrilldownSortLabel(drilldownFilters)); + } + return chips; +} + +function updateAnalyticsRangeHint(range) { + const box = $('analyticsRangeHint'); + if (!box) { + return; + } + const filters = []; + if (state.analytics.queueId !== 'all') { + const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId); + filters.push(`очередь: ${queue?.name || state.analytics.queueId}`); + } + if (state.analytics.channel !== 'all') { + filters.push(`канал: ${analyticsChannelLabel(state.analytics.channel)}`); + } + filters.push( + state.analytics.compareMode === 'previous' + ? 'сравнение с предыдущим окном' + : 'только текущий срез', + ); + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId); + if (activeView) { + filters.push(`вид: ${activeView.name}`); + } + const suffix = filters.length ? ` | ${filters.join(' | ')}` : ''; + box.textContent = range.custom + ? `Окно анализа: ${formatTime(range.current.from.toISOString())} - ${formatTime(range.current.to.toISOString())}${suffix}` + : `Период: ${analyticsPresetLabel(range.preset)}${suffix}`; +} + +function renderAnalyticsNarrative(rangeMeta) { + const box = $('analyticsNarrative'); + if (!box) { + return; + } + const current = state.analytics.overview || emptyKpiEnvelope(); + const previous = state.analytics.compare || emptyKpiEnvelope(); + const total = Number(current?.volume?.total || 0); + const sl = Number(current?.kpi?.SL || 0); + const abandon = Number(current?.kpi?.Abandon || 0); + const digitalShare = Number(current?.kpi?.DigitalShare || 0); + const delta = analyticsDelta('total', current, previous); + const filters = []; + if (state.analytics.channel !== 'all') { + filters.push(analyticsChannelLabel(state.analytics.channel)); + } + if (state.analytics.queueId !== 'all') { + const queue = state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId); + filters.push(queue?.name || state.analytics.queueId); + } + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId); + if (activeView) { + filters.push(activeView.name); + } + + const chips = [ + analyticsPresetLabel(rangeMeta.preset), + analyticsCompareModeLabel(state.analytics.compareMode), + ...filters, + ]; + const deltaBadge = state.analytics.compareMode === 'previous' + ? `${delta.text} к предыдущему окну` + : 'Сравнение отключено'; + + box.innerHTML = ` +
            + ${formatAnalyticsNumber(total, 0)} обращений +

            За выбранный период ${formatAnalyticsNumber(sl, 2)}% потока уложилось в SLA, потери составили ${formatAnalyticsNumber(abandon, 2)}%, а цифровая доля достигла ${formatAnalyticsNumber(digitalShare, 2)}%.

            +
            +
            + ${deltaBadge} +
            + ${chips.map((chip) => `${escapeHtml(chip)}`).join('')} +
            +
            + `; +} + +function renderAnalyticsCompareCard(label, metric, currentPayload, previousPayload) { + const currentValue = metricValueFromPayload(metric, currentPayload); + const previousValue = metricValueFromPayload(metric, previousPayload); + const delta = analyticsDelta(metric, currentPayload, previousPayload); + const note = ANALYTICS_METRIC_META[metric]?.note || ''; + const coverageBadge = analyticsMetricCoverageBadge(metric, currentPayload); + return ` +
            +
            + ${label} +
            + ${coverageBadge} + ${delta.text} +
            +
            +
            +
            + Текущее окно + ${formatAnalyticsMetric(metric, currentValue)} +
            +
            + Предыдущее окно + ${formatAnalyticsMetric(metric, previousValue)} +
            +
            +

            ${note}

            +
            + `; +} + +function renderAnalyticsComparePanel() { + const box = $('analyticsComparePanel'); + if (!box) { + return; + } + if (state.analytics.compareMode !== 'previous') { + box.hidden = true; + box.innerHTML = ''; + return; + } + const current = state.analytics.overview || emptyKpiEnvelope(); + const previous = state.analytics.compare || emptyKpiEnvelope(); + box.hidden = false; + box.innerHTML = ` +
            +
            +

            Сравнение периодов

            +

            Быстрый разрез по главным изменениям между текущим и предыдущим окном без перехода в отдельный отчёт.

            +
            + V2 compare mode +
            +
            + ${[ + renderAnalyticsCompareCard('Обращения', 'total', current, previous), + renderAnalyticsCompareCard('SL', 'SL', current, previous), + renderAnalyticsCompareCard('Среднее ожидание', 'ASA', current, previous), + renderAnalyticsCompareCard('Потери', 'Abandon', current, previous), + ].join('')} +
            + `; +} + +function renderAnalyticsOverview() { + const current = state.analytics.overview || emptyKpiEnvelope(); + const previous = state.analytics.compare || emptyKpiEnvelope(); + $('analyticsOverview').innerHTML = [ + renderAnalyticsCard('Обращения', 'total', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Обращения', + }), + renderAnalyticsCard('Обработано', 'answered', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Обработано', + }), + renderAnalyticsCard('SL', 'SL', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'SL', + }), + renderAnalyticsCard('Среднее ожидание', 'ASA', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Среднее ожидание', + }), + renderAnalyticsCard('Среднее время', 'AHT', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Среднее время', + }), + renderAnalyticsCard('Потери', 'Abandon', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Потери', + }), + renderAnalyticsCard('FCR', 'FCR', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'FCR', + }), + renderAnalyticsCard('Цифровая доля', 'DigitalShare', current, previous, { + metricDrilldown: true, + sourceType: 'metric', + sourceLabel: 'Цифровая доля', + }), + ].join(''); +} + +function voiceNameLanguageLabel(language) { + if (language === 'ru') { + return 'Русский'; + } + if (language === 'kz') { + return 'Казахский'; + } + if (language === 'unknown' || !language) { + return 'Не определён'; + } + return String(language); +} + +function voiceNameAnalyticsMetricDetail(metric, payload) { + const totals = payload?.totals || {}; + const allHandoffs = Number(totals.handoff_confirmed_name || 0) + Number(totals.handoff_unconfirmed_name || 0); + if (metric === 'scenario_calls') { + return `${formatAnalyticsNumber(totals.scenario_calls || 0, 0)} звонков прошли через сценарий`; + } + if (metric === 'start_capture_rate') { + return `${formatAnalyticsNumber(totals.start_obtained || 0, 0)} из ${formatAnalyticsNumber(totals.scenario_calls || 0, 0)} звонков`; + } + if (metric === 'downstream_rescue_rate') { + return `${formatAnalyticsNumber(totals.downstream_ai_obtained || 0, 0)} из ${formatAnalyticsNumber(totals.needed_downstream || 0, 0)} случаев`; + } + if (metric === 'handoff_unconfirmed_rate') { + return `${formatAnalyticsNumber(totals.handoff_unconfirmed_name || 0, 0)} из ${formatAnalyticsNumber(allHandoffs || 0, 0)} передач`; + } + return `${formatAnalyticsNumber(totals.manual_corrected || 0, 0)} из ${formatAnalyticsNumber(allHandoffs || 0, 0)} передач`; +} + +function renderVoiceNameAnalyticsCard(label, metric, currentPayload, previousPayload) { + const value = voiceNameAnalyticsMetricValue(metric, currentPayload); + const showCompare = state.analytics.compareMode === 'previous'; + const delta = showCompare ? voiceNameAnalyticsDelta(metric, currentPayload, previousPayload) : null; + const note = VOICE_NAME_METRIC_META[metric]?.note || ''; + const detail = voiceNameAnalyticsMetricDetail(metric, currentPayload); + return ` +
            +
            +
            ${label}
            + ${showCompare ? `${delta.text}` : ''} +
            +
            ${formatVoiceNameAnalyticsMetric(metric, value)}
            + ${detail ? `
            ${detail}
            ` : ''} + ${note ? `
            ${note}
            ` : ''} +
            + `; +} + +function renderVoiceNameAnalyticsOverview() { + const box = $('voiceNameAnalyticsOverview'); + if (!box) { + return; + } + if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) { + box.innerHTML = ''; + return; + } + const current = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview(); + if (!state.analytics.loading && !Number(current?.totals?.scenario_calls || 0)) { + box.innerHTML = ''; + return; + } + const previous = state.analytics.voiceNameCompare || emptyVoiceNameAnalyticsOverview(); + box.innerHTML = [ + renderVoiceNameAnalyticsCard('Звонки в сценарии', 'scenario_calls', current, previous), + renderVoiceNameAnalyticsCard('Имя взято сразу', 'start_capture_rate', current, previous), + renderVoiceNameAnalyticsCard('Имя добрал AI после follow-up', 'downstream_rescue_rate', current, previous), + renderVoiceNameAnalyticsCard('Передача без подтверждённого имени', 'handoff_unconfirmed_rate', current, previous), + renderVoiceNameAnalyticsCard('Ручное исправление оператором', 'manual_correction_rate', current, previous), + ].join(''); +} + +function renderVoiceNameAnalyticsTrendChart() { + const container = $('voiceNameAnalyticsTrendChart'); + if (!container) { + return; + } + if (!voiceNameAnalyticsSupportedChannel()) { + container.innerHTML = ''; + return; + } + if (state.analytics.voiceNameTrendError && !state.analytics.voiceNameTrend?.points?.length) { + container.innerHTML = `
            Не удалось обновить тренд по voice name-flow: ${escapeHtml(state.analytics.voiceNameTrendError)}
            `; + return; + } + const metric = state.analytics.voiceNameTrendMetric || 'scenario_calls'; + const trend = state.analytics.voiceNameTrend || emptyVoiceNameAnalyticsTimeseries(metric); + const items = Array.isArray(trend.points) ? trend.points : []; + const current = state.analytics.voiceNameOverview || emptyVoiceNameAnalyticsOverview(); + const previous = state.analytics.voiceNameCompare || emptyVoiceNameAnalyticsOverview(); + const delta = state.analytics.compareMode === 'previous' + ? voiceNameAnalyticsDelta(metric, current, previous) + : null; + const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null; + renderTrendChart(container, { + title: voiceNameAnalyticsMetricLabel(metric), + items, + valueAccessor: (item) => item.value, + labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || 'day'), + axisFormatter: (value) => formatVoiceNameAnalyticsAxisValue(metric, value), + valueFormatter: (value) => formatVoiceNameAnalyticsMetric(metric, value), + summaryText: lastPoint + ? `${formatVoiceNameAnalyticsMetric(metric, lastPoint.value)} в последней точке` + : '—', + metaBadge: delta ? `${escapeHtml(delta.text)}` : '', + ariaLabel: 'График voice name-flow аналитики', + emptyText: 'За выбранный период недостаточно точек для анализа сценария сбора имени.', + }); +} + +function renderVoiceNameAnalyticsFunnel() { + const container = $('voiceNameAnalyticsFunnel'); + if (!container) { + return; + } + if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.funnel) + ? state.analytics.voiceNameOverview.breakdowns.funnel + : []; + if (!rows.length) { + container.innerHTML = '
            Нет данных по этапам сценария за выбранный период.
            '; + return; + } + container.innerHTML = ` +
            + Этап + Сессии + Доля +
            + ${rows.map((item) => ` +
            + ${escapeHtml(VOICE_NAME_FUNNEL_LABELS[item.stage] || item.label || item.stage)} + ${formatAnalyticsNumber(item.sessions || 0, 0)} + ${formatAnalyticsNumber(item.share || 0, 2)}% +
            + `).join('')} + `; +} + +function renderVoiceNameAnalyticsLanguageTable() { + const container = $('voiceNameAnalyticsLanguageTable'); + if (!container) { + return; + } + if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.by_language) + ? state.analytics.voiceNameOverview.breakdowns.by_language + : []; + if (!rows.length) { + container.innerHTML = '
            По языкам пока нет достаточных данных.
            '; + return; + } + container.innerHTML = ` +
            + Язык + Звонки + Старт + AI после follow-up + Передача без имени + Ручное исправление +
            + ${rows.map((item) => ` +
            + ${escapeHtml(voiceNameLanguageLabel(item.language))} + ${formatAnalyticsNumber(item.scenario_calls || 0, 0)} + ${formatAnalyticsNumber(item.start_capture_rate || 0, 2)}% + ${formatAnalyticsNumber(item.downstream_rescue_rate || 0, 2)}% + ${formatAnalyticsNumber(item.handoff_unconfirmed_rate || 0, 2)}% + ${formatAnalyticsNumber(item.manual_correction_rate || 0, 2)}% +
            + `).join('')} + `; +} + +function renderVoiceNameAnalyticsQueueTable() { + const container = $('voiceNameAnalyticsQueueTable'); + if (!container) { + return; + } + if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.by_queue) + ? state.analytics.voiceNameOverview.breakdowns.by_queue + : []; + if (!rows.length) { + container.innerHTML = '
            По очередям пока нет данных по voice name-flow.
            '; + return; + } + container.innerHTML = ` +
            + Очередь + Звонки + Старт + AI после follow-up + Передача без имени + Ручное исправление +
            + ${rows.map((item) => ` +
            + ${escapeHtml(analyticsQueueName(item.queue_id))}${escapeHtml(item.queue_id)} + ${formatAnalyticsNumber(item.scenario_calls || 0, 0)} + ${formatAnalyticsNumber(item.start_capture_rate || 0, 2)}% + ${formatAnalyticsNumber(item.downstream_rescue_rate || 0, 2)}% + ${formatAnalyticsNumber(item.handoff_unconfirmed_rate || 0, 2)}% + ${formatAnalyticsNumber(item.manual_correction_rate || 0, 2)}% +
            + `).join('')} + `; +} + +function renderVoiceNameAnalyticsHandoffTable() { + const container = $('voiceNameAnalyticsHandoffTable'); + if (!container) { + return; + } + if (!voiceNameAnalyticsSupportedChannel() || state.analytics.voiceNameError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.voiceNameOverview?.breakdowns?.handoff) + ? state.analytics.voiceNameOverview.breakdowns.handoff + : []; + if (!rows.length) { + container.innerHTML = '
            Разрез по передачам пока пуст для выбранного окна.
            '; + return; + } + container.innerHTML = ` +
            + Итог передачи + Звонки + Доля +
            + ${rows.map((item) => ` +
            + ${escapeHtml(VOICE_NAME_HANDOFF_LABELS[item.outcome] || item.label || item.outcome)} + ${formatAnalyticsNumber(item.sessions || 0, 0)} + ${formatAnalyticsNumber(item.share || 0, 2)}% +
            + `).join('')} + `; +} + +function renderVoiceNameAnalyticsEmptyState() { + const box = $('voiceNameAnalyticsEmptyState'); + if (!box) { + return; + } + if (!voiceNameAnalyticsSupportedChannel()) { + box.hidden = false; + box.textContent = 'Аналитика voice name-flow доступна только для срезов “Все каналы” и “Голос”.'; + return; + } + if (state.analytics.voiceNameError) { + box.hidden = false; + box.textContent = `Не удалось обновить voice name-flow аналитику: ${state.analytics.voiceNameError}`; + return; + } + const total = Number(state.analytics.voiceNameOverview?.totals?.scenario_calls || 0); + if (!state.analytics.loading && total === 0) { + box.hidden = false; + box.textContent = mockVoiceNameSeed().empty_note || 'За выбранный период не найдено звонков, прошедших через сценарий сбора имени.'; + return; + } + box.hidden = true; + box.textContent = ''; +} + +function aiAnalyticsMetricDetail(metric, payload) { + const totals = payload?.totals || {}; + if (metric === 'containment_rate') { + return `${formatAnalyticsNumber(totals.sessions_contained || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`; + } + if (metric === 'handoff_rate') { + return `${formatAnalyticsNumber(totals.sessions_handoff || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`; + } + if (metric === 'ai_latency_avg_ms') { + return `${formatAnalyticsNumber(totals.assistant_turns || 0, 0)} ответов AI`; + } + if (metric === 'closed_without_operator_rate') { + return `${formatAnalyticsNumber(totals.sessions_closed_without_operator || 0, 0)} закрытий без оператора`; + } + return ''; +} + +function renderAiAnalyticsCard(label, metric, currentPayload, previousPayload) { + const value = aiAnalyticsMetricValue(metric, currentPayload); + const showCompare = state.analytics.compareMode === 'previous'; + const delta = showCompare ? aiAnalyticsDelta(metric, currentPayload, previousPayload) : null; + const note = AI_ANALYTICS_METRIC_META[metric]?.note || ''; + const detail = aiAnalyticsMetricDetail(metric, currentPayload); + return ` +
            +
            +
            ${label}
            + ${showCompare ? `${delta.text}` : ''} +
            +
            ${formatAiAnalyticsMetric(metric, value)}
            + ${detail ? `
            ${detail}
            ` : ''} + ${note ? `
            ${note}
            ` : ''} +
            + `; +} + +function renderAiAnalyticsOverview() { + const box = $('aiAnalyticsOverview'); + if (!box) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + box.innerHTML = ''; + return; + } + const current = state.analytics.aiOverview || emptyAiAnalyticsOverview(); + if (!state.analytics.loading && Number(current?.totals?.sessions_started || 0) === 0) { + box.innerHTML = ''; + return; + } + const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview(); + box.innerHTML = [ + renderAiAnalyticsCard('Закрыто AI', 'containment_rate', current, previous), + renderAiAnalyticsCard('Передано оператору', 'handoff_rate', current, previous), + renderAiAnalyticsCard('Задержка AI', 'ai_latency_avg_ms', current, previous), + renderAiAnalyticsCard('Закрыто без оператора', 'closed_without_operator_rate', current, previous), + ].join(''); +} + +function renderAiAnalyticsChannelComparison() { + const container = $('aiAnalyticsChannelComparison'); + const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_channel) + ? state.analytics.aiOverview.breakdowns.by_channel + : []; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!rows.length) { + container.innerHTML = '
            Нет AI-сессий по Telegram и WhatsApp за выбранный период.
            '; + return; + } + + container.innerHTML = ` +
            + Канал + Сессии + Только AI + С оператором + Передача + Средняя задержка +
            + ${rows + .map((item) => { + const aiOnlyShare = item.sessions_started ? (item.ai_only_sessions / item.sessions_started) * 100 : 0; + const humanTouchedShare = item.sessions_started ? (item.human_touched_sessions / item.sessions_started) * 100 : 0; + return ` +
            + ${escapeHtml(analyticsChannelLabel(item.channel))} + ${formatAnalyticsNumber(item.sessions_started, 0)} + ${formatAnalyticsNumber(aiOnlyShare, 2)}% + ${formatAnalyticsNumber(humanTouchedShare, 2)}% + ${formatAnalyticsNumber(item.handoff_rate || 0, 2)}% + ${formatAiAnalyticsMetric('ai_latency_avg_ms', item.ai_latency_avg_ms)} +
            + `; + }) + .join('')} + `; +} + +function renderAiAnalyticsCoverage() { + const container = $('aiAnalyticsCoverage'); + const coverage = state.analytics.aiOverview?.coverage || emptyAiAnalyticsOverview().coverage; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!state.analytics.loading && Number(state.analytics.aiOverview?.totals?.sessions_started || 0) === 0) { + container.innerHTML = ''; + return; + } + container.innerHTML = ` +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_interaction_id || 0, 0)} +

            AI-сессий связаны с ID обращения

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_queue_id || 0, 0)} +

            AI-сессий имеют ID очереди через обращение или диалог

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_latency_turns || 0, 0)} +

            AI-сессий содержат данные по задержке ответов модели

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_terminal_state || 0, 0)} +

            AI-сессий дошли до финального состояния

            +
            +
            + `; +} + +function agentAnalyticsMetricValue(metric, payload = state.analytics.agentOverview || emptyAgentAnalyticsOverview()) { + const totals = payload?.totals || {}; + if (metric === 'agents_with_activity') { + return Number(totals.agents_with_activity || 0); + } + if (metric === 'avg_interactions_per_agent') { + return Number(totals.avg_interactions_per_agent || 0); + } + if (metric === 'avg_handle_seconds') { + return totals.avg_handle_seconds === null || totals.avg_handle_seconds === undefined + ? null + : Number(totals.avg_handle_seconds); + } + if (metric === 'avg_fcr_rate') { + return totals.avg_fcr_rate === null || totals.avg_fcr_rate === undefined + ? null + : Number(totals.avg_fcr_rate); + } + return Number(totals.agents_total || 0); +} + +function formatAgentAnalyticsMetric(metric, value) { + if (value === null || value === undefined) { + return '—'; + } + if (metric === 'avg_handle_seconds') { + return `${formatAnalyticsNumber(value, 1)} с`; + } + if (metric === 'avg_fcr_rate') { + return `${formatAnalyticsNumber(value, 1)}%`; + } + if (metric === 'avg_interactions_per_agent') { + return formatAnalyticsNumber(value, 1); + } + return formatAnalyticsNumber(value, 0); +} + +function agentAnalyticsDelta(metric, currentPayload, previousPayload) { + const current = agentAnalyticsMetricValue(metric, currentPayload); + const previous = agentAnalyticsMetricValue(metric, previousPayload); + const normalizedCurrent = current === null || current === undefined ? 0 : Number(current); + const normalizedPrevious = previous === null || previous === undefined ? 0 : Number(previous); + const diff = normalizedCurrent - normalizedPrevious; + const digits = metric === 'agents_total' || metric === 'agents_with_activity' ? 0 : 1; + let text = signedDelta(diff, digits); + if (metric === 'avg_handle_seconds') { + text = `${text} с`; + } else if (metric === 'avg_fcr_rate') { + text = `${text} п.п.`; + } + let tone = 'neutral'; + if (diff !== 0) { + const higherIsBetter = !['avg_handle_seconds'].includes(metric); + tone = diff > 0 ? (higherIsBetter ? 'positive' : 'negative') : (higherIsBetter ? 'negative' : 'positive'); + } + return { text, tone }; +} + +function agentAnalyticsStateLabel(stateValue) { + const stateLabels = { + READY: 'Готов', + BUSY: 'Занят', + BREAK: 'Перерыв', + OFFLINE: 'Оффлайн', + }; + return stateLabels[stateValue] || stateValue || '—'; +} + +function renderAgentAnalyticsCard(label, metric, currentPayload, previousPayload) { + const value = agentAnalyticsMetricValue(metric, currentPayload); + const delta = state.analytics.compareMode === 'previous' + ? agentAnalyticsDelta(metric, currentPayload, previousPayload) + : null; + return ` +
            +
            +
            ${label}
            + ${delta ? `${delta.text}` : ''} +
            +
            ${formatAgentAnalyticsMetric(metric, value)}
            +
            + `; +} + +function renderAgentAnalyticsOverview() { + const box = $('agentAnalyticsOverview'); + if (!box) { + return; + } + if (state.analytics.agentError) { + box.innerHTML = ''; + return; + } + const current = state.analytics.agentOverview || emptyAgentAnalyticsOverview(); + const previous = state.analytics.agentCompare || emptyAgentAnalyticsOverview(); + const totals = current?.totals || {}; + if (!state.analytics.loading && !Number(totals.agents_total || 0) && !Number(totals.agents_with_activity || 0)) { + box.innerHTML = ''; + return; + } + box.innerHTML = [ + renderAgentAnalyticsCard('Агенты с активностью', 'agents_with_activity', current, previous), + renderAgentAnalyticsCard('Средняя нагрузка', 'avg_interactions_per_agent', current, previous), + renderAgentAnalyticsCard('Средний AHT', 'avg_handle_seconds', current, previous), + renderAgentAnalyticsCard('FCR по агентам', 'avg_fcr_rate', current, previous), + ].join(''); +} + +function renderAgentAnalyticsStateStrip() { + const box = $('agentAnalyticsStateStrip'); + if (!box) { + return; + } + if (state.analytics.agentError) { + box.innerHTML = ''; + return; + } + const current = state.analytics.agentOverview || emptyAgentAnalyticsOverview(); + const byState = current?.state_snapshot?.by_state || {}; + const chips = [ + ['READY', Number(byState.READY || 0)], + ['BUSY', Number(byState.BUSY || 0)], + ['BREAK', Number(byState.BREAK || 0)], + ['OFFLINE', Number(byState.OFFLINE || 0)], + ]; + if (!state.analytics.loading && chips.every(([, value]) => !value)) { + box.innerHTML = ''; + return; + } + box.innerHTML = chips + .map(([code, value]) => `${agentAnalyticsStateLabel(code)}: ${formatAnalyticsNumber(value, 0)}`) + .join(''); +} + +function renderAgentAnalyticsTable() { + const container = $('agentAnalyticsTable'); + if (!container) { + return; + } + if (state.analytics.agentError) { + container.innerHTML = ''; + return; + } + const payload = state.analytics.agentOverview || emptyAgentAnalyticsOverview(); + const rows = Array.isArray(payload.items) ? payload.items : []; + if (!rows.length) { + container.innerHTML = '
            За выбранный период пока нет обращений с назначенным агентом.
            '; + return; + } + const totalAgents = Number(payload?.totals?.agents_total || rows.length); + container.innerHTML = ` +
            + Агент + Статус + Очередь + Обращения + Обработано + AHT + FCR +
            + ${rows + .map((item) => ` + + `) + .join('')} + ${totalAgents > rows.length ? `
            Показаны ${formatAnalyticsNumber(rows.length, 0)} из ${formatAnalyticsNumber(totalAgents, 0)} агентов по текущему окну.
            ` : ''} + `; +} + +function agentAnalyticsTrendLabel(metric) { + const labels = { + interactions_per_agent: 'Нагрузка на агента', + agents_with_activity: 'Агенты с активностью', + avg_handle_seconds: 'Средний AHT', + fcr_rate: 'FCR по агентам', + }; + return labels[metric] || metric || 'Тренд'; +} + +function formatAgentAnalyticsTrendValue(metric, value) { + if (value === null || value === undefined) { + return '—'; + } + if (metric === 'avg_handle_seconds') { + return `${formatAnalyticsNumber(value, 1)} с`; + } + if (metric === 'fcr_rate') { + return `${formatAnalyticsNumber(value, 1)}%`; + } + if (metric === 'interactions_per_agent') { + return formatAnalyticsNumber(value, 2); + } + return formatAnalyticsNumber(value, 0); +} + +async function loadVoiceNameAnalyticsTrend(rangeMeta, requestId) { + const metric = state.analytics.voiceNameTrendMetric || 'scenario_calls'; + const interval = analyticsTimeseriesIntervalForRange(rangeMeta); + if (!voiceNameAnalyticsSupportedChannel()) { + state.analytics.voiceNameTrendError = ''; + return emptyVoiceNameAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + } + try { + const payload = await fetchVoiceNameAnalyticsTimeseries(rangeMeta.current, metric); + if (requestId !== state.analytics.requestId) { + return emptyVoiceNameAnalyticsTimeseries(metric, interval); + } + return payload || emptyVoiceNameAnalyticsTimeseries(metric, interval); + } catch (err) { + state.analytics.voiceNameTrendError = err.message; + return emptyVoiceNameAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + } +} + +async function loadAgentAnalyticsTrend(rangeMeta, requestId) { + const metric = state.analytics.agentTrendMetric || 'interactions_per_agent'; + const interval = analyticsTimeseriesIntervalForRange(rangeMeta); + try { + const payload = await fetchAgentAnalyticsTimeseries(rangeMeta.current, metric, interval); + if (requestId !== state.analytics.requestId) { + return emptyAgentAnalyticsTimeseries(metric, interval); + } + state.analytics.agentTrendInterval = payload?.interval || interval; + return payload || emptyAgentAnalyticsTimeseries(metric, interval); + } catch (err) { + state.analytics.agentTrendError = err.message; + state.analytics.agentTrendInterval = interval; + return emptyAgentAnalyticsTimeseries(metric, interval); + } +} + +function renderAgentAnalyticsTrendChart() { + const container = $('agentAnalyticsTrendChart'); + if (!container) { + return; + } + if (state.analytics.agentError) { + container.innerHTML = ''; + return; + } + if (state.analytics.agentTrendError && !state.analytics.agentTrend?.points?.length) { + container.innerHTML = `
            Не удалось обновить тренд по агентам: ${escapeHtml(state.analytics.agentTrendError)}
            `; + return; + } + const metric = state.analytics.agentTrendMetric || 'interactions_per_agent'; + const trend = state.analytics.agentTrend || emptyAgentAnalyticsTimeseries(metric); + const items = Array.isArray(trend.points) ? trend.points : []; + const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null; + renderTrendChart(container, { + title: agentAnalyticsTrendLabel(metric), + items, + valueAccessor: (item) => item.value, + labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || state.analytics.agentTrendInterval || 'day'), + axisFormatter: (value) => formatAgentAnalyticsTrendValue(metric, value), + valueFormatter: (value) => formatAgentAnalyticsTrendValue(metric, value), + summaryText: lastPoint + ? `${formatAgentAnalyticsTrendValue(metric, lastPoint.value)} в последней точке` + : '—', + metaBadge: '', + ariaLabel: 'График тренда по агентской аналитике', + emptyText: 'За выбранный период недостаточно точек для агентского тренда.', + }); +} + +function renderAgentAnalyticsTeamTable() { + const container = $('agentAnalyticsTeamTable'); + if (!container) { + return; + } + if (state.analytics.agentError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.agentOverview?.breakdowns?.by_team) + ? state.analytics.agentOverview.breakdowns.by_team + : []; + if (!rows.length) { + container.innerHTML = '
            По командам пока нет достаточно данных.
            '; + return; + } + container.innerHTML = ` +
            + Команда + Агенты + Активны + Обращения + AHT + FCR +
            + ${rows + .map((item) => ` +
            + ${escapeHtml(item.label || item.team_key || 'Без очереди')} + ${formatAnalyticsNumber(item.agents_total || 0, 0)} + ${formatAnalyticsNumber(item.agents_with_activity || 0, 0)} + ${formatAnalyticsNumber(item.interactions_total || 0, 0)} + ${formatAgentAnalyticsMetric('avg_handle_seconds', item.avg_handle_seconds)} + ${formatAgentAnalyticsMetric('avg_fcr_rate', item.fcr_rate)} +
            + `) + .join('')} + `; +} + +function renderAgentAnalyticsShiftTable() { + const container = $('agentAnalyticsShiftTable'); + if (!container) { + return; + } + if (state.analytics.agentError) { + container.innerHTML = ''; + return; + } + const rows = Array.isArray(state.analytics.agentOverview?.breakdowns?.by_shift) + ? state.analytics.agentOverview.breakdowns.by_shift + : []; + if (!rows.length) { + container.innerHTML = '
            По сменам пока нет достаточно данных.
            '; + return; + } + container.innerHTML = ` +
            + Смена + Агенты + Обращения + Обработано + AHT + FCR +
            + ${rows + .map((item) => ` +
            + ${escapeHtml(item.label || item.shift_key)} + ${formatAnalyticsNumber(item.agents_with_activity || 0, 0)} + ${formatAnalyticsNumber(item.interactions_total || 0, 0)} + ${formatAnalyticsNumber(item.answered_total || 0, 0)} + ${formatAgentAnalyticsMetric('avg_handle_seconds', item.avg_handle_seconds)} + ${formatAgentAnalyticsMetric('avg_fcr_rate', item.fcr_rate)} +
            + `) + .join('')} + `; +} + +function renderAgentAnalyticsEmptyState() { + const box = $('agentAnalyticsEmptyState'); + if (!box) { + return; + } + if (state.analytics.agentError) { + box.hidden = false; + box.textContent = `Не удалось обновить аналитику по агентам: ${state.analytics.agentError}`; + return; + } + const totals = state.analytics.agentOverview?.totals || emptyAgentAnalyticsOverview().totals; + if (!state.analytics.loading && !Number(totals.agents_total || 0) && !Number(totals.agents_with_activity || 0)) { + box.hidden = false; + box.textContent = 'За выбранный период нет агентской активности по текущим фильтрам. Попробуйте расширить окно анализа или снять часть ограничений.'; + return; + } + box.hidden = true; + box.textContent = ''; +} + +function renderAiAnalyticsEmptyState() { + const box = $('aiAnalyticsEmptyState'); + if (!box) { + return; + } + if (!aiAnalyticsSupportedChannel()) { + box.hidden = false; + box.textContent = 'AI-аналитика доступна только для Telegram и WhatsApp. Снимите фильтр по голосу, веб-чату или электронной почте, чтобы увидеть AI-срез.'; + return; + } + if (state.analytics.aiError) { + box.hidden = false; + box.textContent = `Не удалось обновить AI-аналитику: ${state.analytics.aiError}`; + return; + } + const total = Number(state.analytics.aiOverview?.totals?.sessions_started || 0); + if (!state.analytics.loading && total === 0) { + box.hidden = false; + box.textContent = 'За выбранный период AI-сессий по Telegram и WhatsApp не найдено. Измените период или снимите часть фильтров.'; + return; + } + box.hidden = true; + box.textContent = ''; +} + +function formatAnalyticsAxisValue(metric, value) { + const meta = ANALYTICS_METRIC_META[metric] || { unit: 'count' }; + if (meta.unit === 'count') { + return formatAnalyticsNumber(value, 0); + } + return formatAnalyticsNumber(value, 1); +} + +function formatAiAnalyticsAxisValue(metric, value) { + const meta = AI_ANALYTICS_METRIC_META[metric] || { unit: 'count' }; + if (meta.unit === 'pct') { + return formatAnalyticsNumber(value, 1); + } + if (meta.unit === 'ms') { + return formatAnalyticsNumber(value, 0); + } + return formatAnalyticsNumber(value, 0); +} + +function formatAnalyticsBucketLabel(ts, interval = 'day') { + if (!ts) { + return ''; + } + const date = new Date(ts); + if (Number.isNaN(date.getTime())) { + return String(ts); + } + if (interval === 'hour') { + return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + } + return date.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }); +} + +function resetTrendChartHover(container) { + if (!container) { + return; + } + const tooltip = container.querySelector('[data-chart-tooltip]'); + const summary = container.querySelector('[data-chart-summary]'); + if (summary) { + summary.textContent = summary.dataset.default || ''; + summary.classList.remove('is-hovered'); + } + if (tooltip) { + tooltip.hidden = true; + tooltip.innerHTML = ''; + } + container.querySelectorAll('.analytics-point.is-active').forEach((node) => node.classList.remove('is-active')); +} + +function showTrendChartHover(container, target) { + if (!container || !target) { + return; + } + const tooltip = container.querySelector('[data-chart-tooltip]'); + const summary = container.querySelector('[data-chart-summary]'); + const plot = container.querySelector('.analytics-chart-plot'); + if (!tooltip || !summary || !plot) { + return; + } + const label = target.dataset.label || ''; + const value = target.dataset.value || ''; + const index = target.dataset.index || ''; + const x = Number(target.dataset.x || 0); + const width = Number(target.dataset.width || 720); + const plotWidth = plot.clientWidth || width || 1; + const leftPx = Math.max(54, Math.min(plotWidth - 54, (x / width) * plotWidth)); + + summary.textContent = label && value ? `${label} · ${value}` : value || label || summary.dataset.default || ''; + summary.classList.add('is-hovered'); + tooltip.hidden = false; + tooltip.style.left = `${leftPx}px`; + tooltip.innerHTML = ` + ${escapeHtml(label)} + ${escapeHtml(value)} + `; + + container.querySelectorAll('.analytics-point.is-active').forEach((node) => node.classList.remove('is-active')); + const activePoint = container.querySelector(`.analytics-point[data-chart-point="${escapeHtml(index)}"]`); + if (activePoint) { + activePoint.classList.add('is-active'); + } +} + +function bindTrendChartHover(container) { + if (!container) { + return; + } + const plot = container.querySelector('.analytics-chart-plot'); + if (!plot) { + return; + } + plot.querySelectorAll('.analytics-point-hit').forEach((node) => { + node.addEventListener('mouseenter', () => showTrendChartHover(container, node)); + node.addEventListener('focus', () => showTrendChartHover(container, node)); + }); + plot.addEventListener('mouseleave', () => resetTrendChartHover(container)); + plot.addEventListener('focusout', (event) => { + if (!plot.contains(event.relatedTarget)) { + resetTrendChartHover(container); + } + }); +} + +function renderTrendChart(container, options) { + if (!container) { + return; + } + const items = Array.isArray(options.items) ? options.items : []; + const points = items.map((item, index) => { + const raw = options.valueAccessor(item, index); + const value = raw === null || raw === undefined || Number.isNaN(Number(raw)) ? null : Number(raw); + return { + label: options.labelAccessor(item, index), + value, + }; + }); + const numericValues = points + .filter((point) => point.value !== null) + .map((point) => Number(point.value)); + + if (!points.length || !numericValues.length) { + container.innerHTML = `
            ${options.emptyText}
            `; + return; + } + + const maxValue = Math.max(...numericValues, 1); + const width = 720; + const height = 220; + const paddingLeft = 38; + const paddingRight = 16; + const paddingTop = 16; + const paddingBottom = 36; + const chartWidth = width - paddingLeft - paddingRight; + const chartHeight = height - paddingTop - paddingBottom; + const step = points.length > 1 ? chartWidth / (points.length - 1) : chartWidth; + + const pointFor = (value, index) => { + const x = paddingLeft + index * step; + const y = paddingTop + chartHeight - (value / maxValue) * chartHeight; + return { x, y }; + }; + + const grid = [0, 0.25, 0.5, 0.75, 1] + .map((ratio) => { + const y = paddingTop + chartHeight - ratio * chartHeight; + const labelValue = maxValue * ratio; + return ` + + ${escapeHtml(options.axisFormatter(labelValue))} + `; + }) + .join(''); + + const lineSegments = []; + let currentSegment = []; + let markers = ''; + let hitTargets = ''; + points.forEach((point, index) => { + if (point.value === null) { + if (currentSegment.length > 1) { + lineSegments.push(``); + } + currentSegment = []; + return; + } + const marker = pointFor(point.value, index); + currentSegment.push(`${marker.x},${marker.y}`); + const formattedValue = options.valueFormatter(point.value); + markers += ``; + hitTargets += ``; + }); + if (currentSegment.length > 1) { + lineSegments.push(``); + } + + const labelStep = points.length > 12 ? Math.ceil(points.length / 6) : 1; + const labels = points + .map((point, index) => { + if (index % labelStep !== 0 && index !== points.length - 1) { + return ''; + } + const x = paddingLeft + index * step; + return `${escapeHtml(point.label)}`; + }) + .join(''); + + const summaryText = options.summaryText || ''; + const metaBadge = options.metaBadge || ''; + container.innerHTML = ` +
            + ${escapeHtml(options.title)} +
            + ${metaBadge} + ${escapeHtml(summaryText)} +
            +
            +
            + + + ${grid} + ${lineSegments.join('')} + ${markers} + ${hitTargets} + ${labels} + +
            + `; + resetTrendChartHover(container); + bindTrendChartHover(container); +} + +function renderAnalyticsChannelTable() { + const container = $('analyticsChannelTable'); + const rows = state.analytics.channelRows || []; + if (!rows.length) { + container.innerHTML = '
            Нет данных по каналам за выбранный период.
            '; + return; + } + + container.innerHTML = ` +
            + Канал + Всего + Обработано + Потери + Доля ответа +
            + ${rows + .map((item) => { + const answerRate = item.total ? (item.answered / item.total) * 100 : 0; + return ` + + `; + }) + .join('')} + `; +} + +function renderAnalyticsQueueTable() { + const container = $('analyticsQueueTable'); + const rows = state.analytics.queueRows || []; + if (!rows.length) { + const reason = state.analytics.queueAccessError + ? 'Каталог очередей пока недоступен для этой роли. Можно продолжить анализ без разреза по очередям.' + : 'За выбранное окно пока нет данных по очередям. Попробуйте расширить период или снять часть фильтров.'; + container.innerHTML = `
            ${escapeHtml(reason)}
            `; + return; + } + + container.innerHTML = ` +
            + Очередь + Всего + Обработано + SL + Потери +
            + ${rows + .map((item) => ` + + `) + .join('')} + `; +} + +function renderAnalyticsCoverage() { + const container = $('analyticsCoverage'); + if (!container) { + return; + } + const implemented = Array.isArray(state.analytics.coverage?.implemented_metrics) + ? state.analytics.coverage.implemented_metrics + : []; + const dimensions = Array.isArray(state.analytics.coverage?.dimensions) + ? state.analytics.coverage.dimensions + : []; + const filters = Array.isArray(state.analytics.coverage?.supported_filters) + ? state.analytics.coverage.supported_filters + : []; + + container.innerHTML = ` +
            +
            +

            Уже измеряем

            +
            + ${implemented.length + ? implemented + .map((item) => `${escapeHtml(item.code)} · ${escapeHtml(item.label)}`) + .join('') + : 'Каталог метрик пока не загружен.'} +
            +
            +
            +

            Разрезы

            +
            + ${dimensions.length + ? dimensions.map((item) => `${escapeHtml(item)}`).join('') + : 'Разрезы появятся после загрузки coverage.'} +
            +
            +
            +

            Фильтры

            +
            + ${filters.length + ? filters.map((item) => `${escapeHtml(item)}`).join('') + : 'Фильтры появятся после загрузки coverage.'} +
            +
            +
            +
            +
            + Аналитика по агентам +

            Следующий шаг: персональные метрики по агентам, сменам и командам с детализацией по эффективности.

            +
            +
            + AI-аналитика +

            Добавим долю закрытий AI, передачу оператору, задержку ответа и устойчивость AI-сценариев по каналам.

            +
            +
            + Качество диалогов +

            Здесь появятся оценки качества речи и диалогов на основе скоринга транскриптов и QA-правил.

            +
            + ${ + state.analytics.queueAccessError + ? `
            + Доступ к очередям +

            ${escapeHtml(state.analytics.queueAccessError)}

            +
            ` + : '' + } +
            + `; +} + +function renderAnalyticsCoverage() { + const container = $('analyticsCoverage'); + if (!container) { + return; + } + const implemented = Array.isArray(state.analytics.coverage?.implemented_metrics) + ? state.analytics.coverage.implemented_metrics + : []; + const dimensions = Array.isArray(state.analytics.coverage?.dimensions) + ? state.analytics.coverage.dimensions + : []; + const filters = Array.isArray(state.analytics.coverage?.supported_filters) + ? state.analytics.coverage.supported_filters + : []; + + container.innerHTML = ` +
            +
            +

            Уже измеряем

            +
            + ${implemented.length + ? implemented + .map((item) => `${escapeHtml(item.code)} · ${escapeHtml(item.label)}`) + .join('') + : 'Каталог метрик пока не загружен.'} +
            +
            +
            +

            Разрезы

            +
            + ${dimensions.length + ? dimensions.map((item) => `${escapeHtml(item)}`).join('') + : 'Разрезы появятся после загрузки coverage.'} +
            +
            +
            +

            Фильтры

            +
            + ${filters.length + ? filters.map((item) => `${escapeHtml(item)}`).join('') + : 'Фильтры появятся после загрузки coverage.'} +
            +
            +
            +
            +
            + Аналитика по агентам +

            Следующий шаг: персональные метрики по агентам, сменам и командам с детализацией по эффективности.

            +
            +
            + Прогнозы и алерты +

            Дальше сюда добавим прогноз нагрузки, сигналы по SLA и аномалии по очередям и каналам.

            +
            +
            + Качество диалогов +

            Здесь появятся оценки качества речи и диалогов на основе скоринга транскриптов и QA-правил.

            +
            + ${ + state.analytics.queueAccessError + ? `
            + Доступ к очередям +

            ${escapeHtml(state.analytics.queueAccessError)}

            +
            ` + : '' + } +
            + `; +} + +function csvEscape(value) { + const text = String(value ?? ''); + if (/[",\n]/.test(text)) { + return `"${text.replaceAll('"', '""')}"`; + } + return text; +} + +function exportRowsToCsv(rows) { + const columns = ['section', 'key', 'label', 'current', 'previous', 'delta', 'value', 'note']; + return [ + columns.join(','), + ...rows.map((row) => columns.map((column) => csvEscape(row[column] ?? '')).join(',')), + ].join('\n'); +} + +function downloadCsvFile(csv, filename) { + const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +} + +function buildAnalyticsExportRows() { + const rows = []; + const rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls(); + const current = state.analytics.overview || emptyKpiEnvelope(); + const previous = state.analytics.compare || emptyKpiEnvelope(); + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null; + + rows.push({ + section: 'filters', + key: 'period', + label: 'Период', + value: rangeMeta.custom + ? `${formatTime(rangeMeta.current.from.toISOString())} - ${formatTime(rangeMeta.current.to.toISOString())}` + : analyticsPresetLabel(rangeMeta.preset), + }); + rows.push({ + section: 'filters', + key: 'channel', + label: 'Канал', + value: state.analytics.channel === 'all' ? 'Все каналы' : analyticsChannelLabel(state.analytics.channel), + }); + rows.push({ + section: 'filters', + key: 'queue', + label: 'Очередь', + value: state.analytics.queueId === 'all' + ? 'Все очереди' + : state.analytics.queueOptions.find((item) => item.queue_id === state.analytics.queueId)?.name || state.analytics.queueId, + }); + rows.push({ + section: 'filters', + key: 'compare_mode', + label: 'Сравнение', + value: analyticsCompareModeLabel(state.analytics.compareMode), + }); + if (activeView) { + rows.push({ + section: 'filters', + key: 'saved_view', + label: 'Сохранённый вид', + value: activeView.name, + }); + } + + [ + ['total', 'Обращения'], + ['answered', 'Обработано'], + ['SL', 'SL'], + ['ASA', 'Среднее ожидание'], + ['AHT', 'Среднее время'], + ['Abandon', 'Потери'], + ['FCR', 'FCR'], + ['DigitalShare', 'Цифровая доля'], + ].forEach(([metric, label]) => { + const delta = state.analytics.compareMode === 'previous' + ? analyticsDelta(metric, current, previous).text + : ''; + rows.push({ + section: 'overview', + key: metric, + label, + current: formatAnalyticsMetric(metric, metricValueFromPayload(metric, current)), + previous: state.analytics.compareMode === 'previous' + ? formatAnalyticsMetric(metric, metricValueFromPayload(metric, previous)) + : '', + delta, + note: ANALYTICS_METRIC_META[metric]?.note || '', + }); + }); + + state.analytics.channelRows.forEach((item) => { + const answerRate = item.total ? (item.answered / item.total) * 100 : 0; + rows.push({ + section: 'channels', + key: item.channel, + label: analyticsChannelLabel(item.channel), + current: formatAnalyticsNumber(item.total, 0), + previous: formatAnalyticsNumber(item.answered, 0), + delta: formatAnalyticsNumber(item.abandoned, 0), + value: `${formatAnalyticsNumber(answerRate, 2)}%`, + note: 'current=всего, previous=обработано, delta=потери, value=доля ответа', + }); + }); + + state.analytics.queueRows.forEach((item) => { + rows.push({ + section: 'queues', + key: item.queue_id, + label: item.name || item.queue_id, + current: formatAnalyticsNumber(item.total, 0), + previous: formatAnalyticsNumber(item.answered, 0), + delta: `${formatAnalyticsNumber(item.SL, 2)}%`, + value: `${formatAnalyticsNumber(item.Abandon, 2)}%`, + note: 'current=всего, previous=обработано, delta=SL, value=потери', + }); + }); + + state.analytics.trend.forEach((item) => { + rows.push({ + section: 'trend', + key: item.label, + label: analyticsMetricLabel(state.analytics.trendMetric), + value: formatAnalyticsMetric(state.analytics.trendMetric, metricValueFromPayload(state.analytics.trendMetric, item.payload)), + note: item.label, + }); + }); + + (state.analytics.coverage?.implemented_metrics || []).forEach((item) => { + rows.push({ + section: 'coverage', + key: item.code, + label: item.label, + value: 'implemented', + }); + }); + + return rows; +} + +function exportAnalyticsCsv() { + const rows = buildAnalyticsExportRows(); + const dateStamp = new Date().toISOString().slice(0, 10); + downloadCsvFile(exportRowsToCsv(rows), `konturcc-analytics-${dateStamp}.csv`); + state.analytics.statusMessage = `CSV выгружен в ${new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}.`; + renderSavedAnalyticsViews(); +} + +function exportDrilldownRowsToCsv(rows) { + const columns = [ + 'interaction_id', + 'subject', + 'channel', + 'status', + 'queue_id', + 'assigned_to', + 'created_at', + 'updated_at', + 'answered', + 'abandoned', + 'wait_seconds', + 'handle_seconds', + 'within_sla', + 'resolved_first_contact', + ]; + return [ + columns.join(','), + ...rows.map((row) => columns.map((column) => csvEscape(row[column] ?? '')).join(',')), + ].join('\n'); +} + +const exportAnalyticsDrilldownCsvBase = exportAnalyticsDrilldownCsv; +const renderAnalyticsDrilldownBase = renderAnalyticsDrilldown; +const loadAnalyticsDrilldownDetailsBase = loadAnalyticsDrilldownDetails; +const loadAnalyticsDrilldownPageBase = loadAnalyticsDrilldownPage; +const openAnalyticsDrilldownBase = openAnalyticsDrilldown; +const handleAnalyticsDrilldownListClickBase = handleAnalyticsDrilldownListClick; + +exportAnalyticsDrilldownCsv = async function exportAnalyticsDrilldownCsv() { + if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.exporting) { + return; + } + if (!state.drilldown.total) { + state.drilldown.notice = 'Для текущего среза нет строк для экспорта.'; + renderAnalyticsDrilldown(); + return; + } + + state.drilldown.exporting = true; + state.drilldown.notice = ''; + state.drilldown.error = ''; + renderAnalyticsDrilldown(); + + try { + const exportLimit = Math.min(state.drilldown.total, DRILLDOWN_EXPORT_SOFT_CAP); + const rows = []; + for (let currentOffset = 0; currentOffset < exportLimit; currentOffset += DRILLDOWN_EXPORT_BATCH_SIZE) { + const batchLimit = Math.min(DRILLDOWN_EXPORT_BATCH_SIZE, exportLimit - currentOffset); + const payload = state.drilldown.mode === 'metric' + ? await fetchAnalyticsMetricDrilldown(state.drilldown.filters, batchLimit, currentOffset) + : await api( + 'interaction', + `interactions/drilldown?${analyticsDrilldownQuery(state.drilldown.filters, batchLimit, currentOffset)}`, + ); + const batchItems = Array.isArray(payload?.items) ? payload.items : []; + rows.push(...batchItems); + if (!batchItems.length || batchItems.length < batchLimit) { + break; + } + } + const dateStamp = new Date().toISOString().slice(0, 10); + downloadCsvFile( + exportDrilldownRowsToCsv(rows.slice(0, DRILLDOWN_EXPORT_SOFT_CAP)), + `konturcc-drilldown-${analyticsDrilldownFilenameSource()}-${dateStamp}.csv`, + ); + state.drilldown.notice = state.drilldown.total > DRILLDOWN_EXPORT_SOFT_CAP + ? `Экспортирован только первый ${DRILLDOWN_EXPORT_SOFT_CAP} строк из ${formatAnalyticsNumber(state.drilldown.total, 0)}.` + : `Экспорт готов: ${formatAnalyticsNumber(rows.length, 0)} строк.`; + } catch (err) { + state.drilldown.error = err.message; + } finally { + state.drilldown.exporting = false; + renderAnalyticsDrilldown(); + } +} + +function buildTrendBuckets(range, preset, custom) { + const buckets = []; + const hourMs = 60 * 60 * 1000; + const dayMs = 24 * hourMs; + + if (custom) { + const fromDay = startOfDay(range.from); + const endDay = startOfDay(addDays(range.to, 1)); + const totalDays = Math.max(1, Math.ceil((endDay.getTime() - fromDay.getTime()) / dayMs)); + const bucketDays = Math.max(1, Math.ceil(totalDays / 31)); + for (let cursor = new Date(fromDay); cursor < endDay; cursor = new Date(cursor.getTime() + bucketDays * dayMs)) { + const bucketFrom = new Date(cursor); + const bucketTo = new Date(Math.min(bucketFrom.getTime() + bucketDays * dayMs, endDay.getTime())); + const lastDay = new Date(bucketTo.getTime() - dayMs); + const label = bucketDays === 1 + ? bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }) + : `${bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })} - ${lastDay.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' })}`; + buckets.push({ label, range: { from: bucketFrom, to: bucketTo } }); + } + return buckets.slice(0, 31); + } + + if (preset === 'today') { + const dayStart = startOfDay(range.from); + for (let hour = 0; hour < 24; hour += 1) { + const bucketFrom = new Date(dayStart.getTime() + hour * hourMs); + const bucketTo = new Date(bucketFrom.getTime() + hourMs); + buckets.push({ + label: `${pad2(hour)}:00`, + range: { from: bucketFrom, to: bucketTo }, + }); + } + return buckets; + } + + const days = preset === '30d' ? 30 : 7; + const firstDay = startOfDay(range.from); + for (let day = 0; day < days; day += 1) { + const bucketFrom = new Date(firstDay.getTime() + day * dayMs); + const bucketTo = new Date(bucketFrom.getTime() + dayMs); + buckets.push({ + label: bucketFrom.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }), + range: { from: bucketFrom, to: bucketTo }, + }); + } + return buckets; +} + +async function loadAnalyticsTrend(rangeMeta, requestId) { + const buckets = buildTrendBuckets(rangeMeta.current, rangeMeta.preset, rangeMeta.custom); + const items = await Promise.all( + buckets.map(async (bucket) => { + try { + const payload = await fetchAnalyticsKpi(bucket.range); + return { ...bucket, payload }; + } catch { + return { ...bucket, payload: emptyKpiEnvelope(bucket.range.from.toISOString(), bucket.range.to.toISOString()) }; + } + }), + ); + if (requestId !== state.analytics.requestId) { + return []; + } + return items; +} + +function aiAnalyticsIntervalForRange(rangeMeta) { + const durationMs = Math.max(0, rangeMeta.current.to.getTime() - rangeMeta.current.from.getTime()); + return durationMs <= 36 * 60 * 60 * 1000 ? 'hour' : 'day'; +} + +async function loadAiAnalyticsTrend(rangeMeta, requestId) { + const metric = state.analytics.aiTrendMetric || 'containment_rate'; + const interval = aiAnalyticsIntervalForRange(rangeMeta); + if (!aiAnalyticsSupportedChannel()) { + return emptyAiAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + } + + try { + const payload = await fetchAiAnalyticsTimeseries(rangeMeta.current, metric, interval); + if (requestId !== state.analytics.requestId) { + return emptyAiAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + } + return payload || emptyAiAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + } catch (err) { + if (requestId === state.analytics.requestId) { + state.analytics.aiError = err.message; + } + return emptyAiAnalyticsTimeseries( + metric, + interval, + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + } +} + +async function loadAnalyticsQueueRows(rangeMeta, queues, requestId) { + if (!queues.length) { + return []; + } + const rows = await Promise.all( + queues.map(async (queue) => { + try { + const payload = await fetchAnalyticsKpi(rangeMeta.current, { + queueId: queue.queue_id, + channel: state.analytics.channel, + }); + return { + queue_id: queue.queue_id, + name: queue.name || queue.queue_id, + total: Number(payload?.volume?.total || 0), + answered: Number(payload?.volume?.answered || 0), + SL: Number(payload?.kpi?.SL || 0), + Abandon: Number(payload?.kpi?.Abandon || 0), + }; + } catch { + return { + queue_id: queue.queue_id, + name: queue.name || queue.queue_id, + total: 0, + answered: 0, + SL: 0, + Abandon: 0, + }; + } + }), + ); + if (requestId !== state.analytics.requestId) { + return []; + } + return rows.sort((a, b) => b.total - a.total || a.name.localeCompare(b.name)); +} + +function renderAnalyticsTrendChart() { + const metric = state.analytics.trendMetric || 'volume'; + const items = state.analytics.trend || []; + const container = $('analyticsTrendChart'); + if (!items.length) { + container.innerHTML = '
            За выбранный период недостаточно точек для построения тренда. Попробуйте расширить окно анализа.
            '; + return; + } + + const values = items.map((item) => metricValueFromPayload(metric, item.payload)); + const maxValue = Math.max(...values, 1); + const width = 720; + const height = 220; + const paddingLeft = 38; + const paddingRight = 16; + const paddingTop = 16; + const paddingBottom = 36; + const chartWidth = width - paddingLeft - paddingRight; + const chartHeight = height - paddingTop - paddingBottom; + const step = items.length > 1 ? chartWidth / (items.length - 1) : chartWidth; + + const pointFor = (value, index) => { + const x = paddingLeft + index * step; + const y = paddingTop + chartHeight - (value / maxValue) * chartHeight; + return { x, y }; + }; + + const polyline = values + .map((value, index) => { + const point = pointFor(value, index); + return `${point.x},${point.y}`; + }) + .join(' '); + + const grid = [0, 0.25, 0.5, 0.75, 1] + .map((ratio) => { + const y = paddingTop + chartHeight - ratio * chartHeight; + const labelValue = maxValue * ratio; + return ` + + ${escapeHtml(formatAnalyticsNumber(labelValue, metric === 'volume' ? 0 : 1))} + `; + }) + .join(''); + + const markers = values + .map((value, index) => { + const point = pointFor(value, index); + return ``; + }) + .join(''); + + const labelStep = items.length > 12 ? Math.ceil(items.length / 6) : 1; + const labels = items + .map((item, index) => { + if (index % labelStep !== 0 && index !== items.length - 1) { + return ''; + } + const point = pointFor(values[index], index); + return `${escapeHtml(item.label)}`; + }) + .join(''); + + container.innerHTML = ` +
            + ${escapeHtml(analyticsMetricLabel(metric))} + ${escapeHtml(formatAnalyticsMetric(metric, values[values.length - 1] || 0))} в последней точке +
            + + ${grid} + + ${markers} + ${labels} + + `; +} + +function renderAnalyticsTrendChart() { + const metric = state.analytics.trendMetric || 'volume'; + const items = state.analytics.trend || []; + const container = $('analyticsTrendChart'); + if (!container) { + return; + } + const lastValue = items.length + ? metricValueFromPayload(metric, items[items.length - 1].payload) + : null; + renderTrendChart(container, { + title: analyticsMetricLabel(metric), + items, + valueAccessor: (item) => metricValueFromPayload(metric, item.payload), + labelAccessor: (item) => item.label, + axisFormatter: (value) => formatAnalyticsAxisValue(metric, value), + valueFormatter: (value) => formatAnalyticsMetric(metric, value), + summaryText: lastValue === null || lastValue === undefined + ? '—' + : `${formatAnalyticsMetric(metric, lastValue)} в последней точке`, + ariaLabel: 'Analytics trend chart', + emptyText: 'За выбранный период недостаточно точек для построения тренда. Попробуйте расширить окно анализа.', + }); +} + +function renderAiAnalyticsTrendChart() { + const container = $('aiAnalyticsTrendChart'); + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel()) { + container.innerHTML = ''; + return; + } + if (state.analytics.aiError && !state.analytics.aiTrend?.points?.length) { + container.innerHTML = `
            Не удалось обновить AI-тренд: ${escapeHtml(state.analytics.aiError)}
            `; + return; + } + + const metric = state.analytics.aiTrendMetric || 'containment_rate'; + const trend = state.analytics.aiTrend || emptyAiAnalyticsTimeseries(metric); + const items = Array.isArray(trend.points) ? trend.points : []; + const current = state.analytics.aiOverview || emptyAiAnalyticsOverview(); + const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview(); + const delta = state.analytics.compareMode === 'previous' + ? aiAnalyticsDelta(metric, current, previous) + : null; + const lastPoint = [...items].reverse().find((item) => item.value !== null && item.value !== undefined) || null; + const emptyText = metric === 'ai_latency_avg_ms' + ? 'Для выбранного окна пока нет данных по задержке ответов модели. Попробуйте изменить период или канал.' + : 'За выбранный период недостаточно AI-точек для построения тренда.'; + renderTrendChart(container, { + title: aiAnalyticsMetricLabel(metric), + items, + valueAccessor: (item) => item.value, + labelAccessor: (item) => formatAnalyticsBucketLabel(item.ts, trend.interval || 'day'), + axisFormatter: (value) => formatAiAnalyticsAxisValue(metric, value), + valueFormatter: (value) => formatAiAnalyticsMetric(metric, value), + summaryText: lastPoint + ? `${formatAiAnalyticsMetric(metric, lastPoint.value)} в последней точке` + : '—', + metaBadge: delta ? `${escapeHtml(delta.text)}` : '', + ariaLabel: 'График тренда AI-аналитики', + emptyText, + }); +} + +function analyticsDrilldownFiltersForCurrentSource() { + const filters = analyticsDrilldownBaseFilters(); + if (state.drilldown.mode === 'metric' && state.drilldown.metric) { + filters.metric = state.drilldown.metric; + filters.q = ''; + filters.status = ''; + } + if (state.drilldown.sourceType === 'channel' && state.drilldown.sourceValue) { + filters.channel = state.drilldown.sourceValue; + } + if (state.drilldown.sourceType === 'queue' && state.drilldown.sourceValue) { + filters.queue_id = state.drilldown.sourceValue; + } + if (state.drilldown.sourceType === 'agent' && state.drilldown.sourceValue) { + filters.agent_id = state.drilldown.sourceValue; + } + return filters; +} + +function applyAnalyticsDrilldownLocalFilters(nextFilters, options = {}) { + if (!state.drilldown.open || state.drilldown.mode !== 'interaction') { + return; + } + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(nextFilters); + state.drilldown.offset = 0; + state.drilldown.notice = ''; + void loadAnalyticsDrilldownPage({ preserveSelection: options.preserveSelection !== false }); +} + +function resetAnalyticsDrilldownLocalFilters() { + clearAnalyticsDrilldownSearchDebounce(); + if (!state.drilldown.open || state.drilldown.mode !== 'interaction') { + return; + } + state.drilldown.filters = analyticsDrilldownFiltersForCurrentSource(); + state.drilldown.offset = 0; + state.drilldown.notice = ''; + renderAnalyticsDrilldown(); + void loadAnalyticsDrilldownPage({ preserveSelection: true }); +} + +function handleAnalyticsDrilldownSearchInput(event) { + if (!state.drilldown.open || state.drilldown.mode !== 'interaction') { + return; + } + clearAnalyticsDrilldownSearchDebounce(); + const query = event.target?.value || ''; + analyticsDrilldownSearchDebounce = window.setTimeout(() => { + if (!state.drilldown.open || !state.drilldown.filters) { + return; + } + const normalizedQuery = query.trim(); + if ((state.drilldown.filters.q || '') === normalizedQuery) { + return; + } + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + q: normalizedQuery, + }); + }, 300); +} + +function handleAnalyticsDrilldownSearchKeydown(event) { + if (event.key !== 'Enter' || !state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') { + return; + } + event.preventDefault(); + clearAnalyticsDrilldownSearchDebounce(); + const normalizedQuery = (event.target?.value || '').trim(); + if ((state.drilldown.filters.q || '') === normalizedQuery) { + return; + } + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + q: normalizedQuery, + }); +} + +function handleAnalyticsDrilldownStatusChange(event) { + if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') { + return; + } + const nextStatus = event.target?.value === 'all' ? '' : String(event.target?.value || ''); + if ((state.drilldown.filters.status || '') === nextStatus) { + return; + } + clearAnalyticsDrilldownSearchDebounce(); + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + status: nextStatus, + }); +} + +function handleAnalyticsDrilldownSortChange(event) { + if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode !== 'interaction') { + return; + } + const nextSort = parseAnalyticsDrilldownSort(event.target?.value || 'created_at:desc'); + if ( + (state.drilldown.filters.sort_by || 'created_at') === nextSort.sort_by + && (state.drilldown.filters.sort_dir || 'desc') === nextSort.sort_dir + ) { + return; + } + clearAnalyticsDrilldownSearchDebounce(); + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + ...nextSort, + }); +} + +function openAnalyticsDrilldown(sourceType, sourceValue = '', options = {}) { + const mode = options.mode || (sourceType === 'metric' ? 'metric' : 'interaction'); + if (mode !== 'ai') { + return openAnalyticsDrilldownBase(sourceType, sourceValue, options); + } + const filters = analyticsDrilldownBaseFilters(); + filters.slice = options.slice || sourceValue || 'all'; + filters.reason_key = options.reasonKey || ''; + filters.status = ''; + filters.q = ''; + filters.sort_by = 'created_at'; + filters.sort_dir = 'desc'; + if (sourceType === 'ai-channel' && sourceValue) { + filters.channel = sourceValue; + } + if (sourceType === 'ai-reason' && sourceValue) { + filters.slice = 'handoff'; + filters.reason_key = sourceValue; + } + rememberAnalyticsDrilldownReturnFocus(); + state.drilldown = { + ...emptyDrilldownState(), + open: true, + mode: 'ai', + sourceType, + sourceLabel: options.sourceLabel || analyticsDrilldownSourceLabel(sourceType, sourceValue), + sourceValue, + filters: normalizeAnalyticsDrilldownFilters(filters), + coverage: options.coverage || null, + metricNote: options.metricNote || aiAnalyticsDrilldownNote(filters), + }; + renderAnalyticsDrilldown(); + window.requestAnimationFrame(() => { + focusAnalyticsDrilldownPrimaryControl(true); + }); + void loadAnalyticsDrilldownPage(); +} + +async function loadAnalyticsDrilldownPage(options = {}) { + if (state.drilldown.mode !== 'ai') { + return loadAnalyticsDrilldownPageBase(options); + } + if (!state.drilldown.open || !state.drilldown.filters) { + return; + } + const preserveSelection = Boolean(options.preserveSelection); + state.drilldown.loading = true; + state.drilldown.error = ''; + renderAnalyticsDrilldown(); + try { + const data = await fetchAiAnalyticsDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset); + if (!state.drilldown.open) { + return; + } + state.drilldown.items = Array.isArray(data?.items) ? data.items : []; + state.drilldown.total = Number(data?.total || 0); + state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE); + state.drilldown.offset = Number(data?.offset || 0); + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters); + state.drilldown.coverage = data?.coverage || state.drilldown.coverage; + state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters); + + const selectedId = state.drilldown.selectedInteractionId || ''; + const selectedStillVisible = preserveSelection + && selectedId + && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId); + if (selectedStillVisible) { + state.drilldown.selectedInteraction = state.drilldown.items.find( + (item) => analyticsDrilldownCurrentItemId(item) === selectedId, + ) || state.drilldown.selectedInteraction; + void loadAnalyticsDrilldownDetails(selectedId); + } else if (state.drilldown.items.length) { + state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]); + state.drilldown.selectedInteraction = state.drilldown.items[0]; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else { + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + } + } catch (err) { + if (!state.drilldown.open) { + return; + } + state.drilldown.items = []; + state.drilldown.total = 0; + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open) { + state.drilldown.loading = false; + renderAnalyticsDrilldown(); + } + } +} + +async function loadAnalyticsDrilldownDetails(interactionId) { + if (state.drilldown.mode !== 'ai') { + return loadAnalyticsDrilldownDetailsBase(interactionId); + } + if (!state.drilldown.open || !interactionId) { + return; + } + state.drilldown.selectedInteractionId = interactionId; + state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.session_id === interactionId) || null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.error = ''; + state.drilldown.detailLoading = true; + renderAnalyticsDrilldown(); + + try { + const detail = await fetchAiAnalyticsSessionDetail(interactionId); + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) { + return; + } + state.drilldown.selectedInteraction = { + ...(state.drilldown.selectedInteraction || {}), + ...(detail?.session || {}), + }; + state.drilldown.selectedLinkedInteraction = detail?.interaction || null; + state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : []; + } catch (err) { + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) { + return; + } + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open && state.drilldown.selectedInteractionId === interactionId) { + state.drilldown.detailLoading = false; + renderAnalyticsDrilldown(); + } + } +} + +function handleAnalyticsDrilldownListClick(event) { + if (state.drilldown.mode !== 'ai') { + return handleAnalyticsDrilldownListClickBase(event); + } + const button = event.target.closest('[data-analytics-row-id]'); + if (!button) { + return; + } + maybeScrollAnalyticsDrilldownDetailIntoView(); + void loadAnalyticsDrilldownDetails(button.dataset.analyticsRowId || ''); +} + +function handleAiAnalyticsOverviewClick(event) { + const button = event.target.closest('[data-ai-drilldown-slice]'); + if (!button) { + return; + } + const slice = button.dataset.aiDrilldownSlice || 'all'; + openAnalyticsDrilldown('ai-overview', slice, { + mode: 'ai', + slice, + sourceLabel: button.dataset.aiDrilldownLabel || aiAnalyticsSliceLabel(slice), + }); +} + +function handleAiAnalyticsChannelClick(event) { + const button = event.target.closest('[data-ai-channel]'); + if (!button) { + return; + } + const channel = button.dataset.aiChannel || ''; + openAnalyticsDrilldown('ai-channel', channel, { + mode: 'ai', + slice: 'all', + sourceLabel: `AI-канал: ${analyticsChannelLabel(channel)}`, + }); +} + +function handleAiAnalyticsOutcomeClick(event) { + const button = event.target.closest('[data-ai-outcome]'); + if (!button) { + return; + } + const slice = button.dataset.aiOutcome || 'all'; + openAnalyticsDrilldown('ai-overview', slice, { + mode: 'ai', + slice, + sourceLabel: button.dataset.aiOutcomeLabel || aiAnalyticsSliceLabel(slice), + }); +} + +function handleAiAnalyticsReasonClick(event) { + const button = event.target.closest('[data-ai-reason-key]'); + if (!button) { + return; + } + const reasonKey = button.dataset.aiReasonKey || ''; + openAnalyticsDrilldown('ai-reason', reasonKey, { + mode: 'ai', + slice: 'handoff', + reasonKey, + sourceLabel: button.dataset.aiReasonLabel || `Причина передачи: ${reasonKey}`, + }); +} + +function renderAiAnalyticsDrilldownFactBadges(item) { + if (!item || typeof item !== 'object') { + return ''; + } + const badges = []; + if (item.contained) { + badges.push({ tone: 'sla', label: 'Закрыто AI' }); + } + if (item.handoff) { + badges.push({ tone: 'abandoned', label: 'Передано оператору' }); + } + if (item.human_touched) { + badges.push({ tone: 'handle', label: 'С участием оператора' }); + } + if (item.closed_without_operator) { + badges.push({ tone: 'fcr', label: 'Закрыто без оператора' }); + } + if (item.ai_latency_avg_ms !== null && item.ai_latency_avg_ms !== undefined) { + badges.push({ tone: 'wait', label: `Задержка ${formatAnalyticsNumber(item.ai_latency_avg_ms, 0)} мс` }); + } + if (item.reason_label) { + badges.push({ tone: 'handle', label: item.reason_label }); + } + if (!badges.length) { + return ''; + } + return ` +
            + ${badges + .map( + (badge) => `${escapeHtml(badge.label)}`, + ) + .join('')} +
            + `; +} + +renderAnalyticsDrilldown = function renderAnalyticsDrilldown() { + if (state.drilldown.mode !== 'ai') { + return renderAnalyticsDrilldownBase(); + } + const drawer = $('analyticsDrilldownDrawer'); + const backdrop = $('analyticsDrilldownBackdrop'); + const closeBtn = $('analyticsDrilldownCloseBtn'); + const title = $('analyticsDrilldownTitle'); + const meta = $('analyticsDrilldownMeta'); + const filters = $('analyticsDrilldownFilters'); + const list = $('analyticsDrilldownList'); + const detail = $('analyticsDrilldownDetail'); + const prevBtn = $('analyticsDrilldownPrevBtn'); + const nextBtn = $('analyticsDrilldownNextBtn'); + const pageMeta = $('analyticsDrilldownPageMeta'); + const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong'); + const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong'); + const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint'); + if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) { + return; + } + + const isOpen = state.drilldown.open; + document.body.classList.toggle('analytics-drilldown-open', isOpen); + drawer.classList.toggle('is-open', isOpen); + backdrop.classList.toggle('is-open', isOpen); + drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + closeBtn.classList.add('analytics-drawer-close'); + closeBtn.setAttribute('aria-label', 'Закрыть детализацию'); + closeBtn.textContent = 'Закрыть · Esc'; + drawer.hidden = !isOpen; + backdrop.hidden = !isOpen; + if (!isOpen) { + title.textContent = 'Детализация AI-сессий'; + meta.textContent = 'Выберите карточку, исход, причину передачи или канал, чтобы открыть срез по AI-сессиям.'; + filters.innerHTML = ''; + list.innerHTML = ''; + detail.innerHTML = ''; + pageMeta.textContent = '0 из 0'; + prevBtn.disabled = true; + nextBtn.disabled = true; + if (listHeading) { + listHeading.textContent = 'Список AI-сессий'; + } + if (detailHeading) { + detailHeading.textContent = 'Детали AI-сессии'; + } + if (detailHint) { + detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.'; + } + syncAnalyticsDrilldownControls(); + return; + } + + const chips = analyticsDrilldownChips(); + const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0; + const currentPageEnd = state.drilldown.total + ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total) + : 0; + title.textContent = state.drilldown.sourceLabel || 'Детализация AI-сессий'; + if (listHeading) { + listHeading.textContent = 'Список AI-сессий'; + } + if (detailHeading) { + detailHeading.textContent = 'Детали AI-сессии'; + } + if (detailHint) { + detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.'; + } + meta.textContent = state.drilldown.error + ? `Не удалось обновить детализацию AI: ${state.drilldown.error}` + : state.drilldown.exporting + ? 'Готовим CSV по текущему AI-срезу.' + : state.drilldown.loading + ? 'Загружаем AI-сессии и ленту метаданных...' + : `${formatAnalyticsNumber(state.drilldown.total, 0)} AI-сессий в выбранном срезе.`; + filters.innerHTML = [ + ...chips.map((chip) => `${escapeHtml(chip)}`), + state.drilldown.notice + ? `${escapeHtml(state.drilldown.notice)}` + : '', + ].join(''); + pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`; + prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0; + nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total; + syncAnalyticsDrilldownControls(); + + if (state.drilldown.loading && !state.drilldown.items.length) { + list.innerHTML = '
            Загружаем AI-сессии...
            '; + } else if (!state.drilldown.items.length) { + list.innerHTML = '
            По этому AI-срезу сессий пока нет.
            '; + } else { + list.innerHTML = state.drilldown.items + .map((item) => ` + + `) + .join(''); + } + + const selectedSession = state.drilldown.selectedInteraction + || state.drilldown.items.find((item) => item.session_id === state.drilldown.selectedInteractionId) + || null; + if (!selectedSession) { + detail.innerHTML = '
            Выберите AI-сессию в списке, чтобы увидеть детали на уровне метаданных.
            '; + return; + } + + const timelineMarkup = state.drilldown.detailLoading + ? '
            Загружаем ленту метаданных...
            ' + : state.drilldown.selectedTimeline.length + ? state.drilldown.selectedTimeline + .map((event) => { + const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata); + const metadataMarkup = Object.keys(safeMetadata).length + ? `
            ${escapeHtml(JSON.stringify(safeMetadata, null, 2))}
            ` + : '

            Только событие уровня метаданных без текста сообщений.

            '; + return ` +
            +
            + ${escapeHtml(event.label || event.event_type || 'событие')} + ${escapeHtml(formatTime(event.ts))} +
            + ${metadataMarkup} +
            + `; + }) + .join('') + : '
            Metadata timeline по этой AI session пока пуст.
            '; + + const linkedInteractionMarkup = state.drilldown.selectedLinkedInteraction + ? ` +
            +
            +
            + Связанное обращение +

            ${escapeHtml(state.drilldown.selectedLinkedInteraction.interaction_id || '—')}

            +
            +
            +
            +
            Канал${escapeHtml(analyticsChannelLabel(state.drilldown.selectedLinkedInteraction.channel))}
            +
            Очередь${escapeHtml(analyticsQueueName(state.drilldown.selectedLinkedInteraction.queue_id))}
            +
            Статус${escapeHtml(interactionStatusLabel(state.drilldown.selectedLinkedInteraction.status))}
            +
            Назначен${escapeHtml(state.drilldown.selectedLinkedInteraction.assigned_to || 'Не назначен')}
            +
            Тема${escapeHtml(state.drilldown.selectedLinkedInteraction.subject || '—')}
            +
            Обновлено${escapeHtml(formatTime(state.drilldown.selectedLinkedInteraction.updated_at))}
            +
            +
            + ` + : ''; + + detail.innerHTML = ` +
            +
            +
            + ${escapeHtml(selectedSession.session_id)} +

            ${escapeHtml(aiAnalyticsSliceLabel(state.drilldown.filters?.slice || 'all'))}

            +
            +
            + ${escapeHtml(aiAnalyticsStatusLabel(selectedSession.status))} +
            +
            +
            +

            ${escapeHtml(aiAnalyticsDrilldownNote(state.drilldown.filters))}

            +
            + ${renderAiAnalyticsDrilldownFactBadges(selectedSession)} +
            +
            Канал${escapeHtml(analyticsChannelLabel(selectedSession.channel))}
            +
            Очередь${escapeHtml(analyticsQueueName(selectedSession.queue_id))}
            +
            ID обращения${escapeHtml(selectedSession.interaction_id || '—')}
            +
            ID диалога${escapeHtml(selectedSession.thread_id || '—')}
            +
            Назначено${escapeHtml(selectedSession.assigned_to || 'Не назначен')}
            +
            Взял оператор${escapeHtml(selectedSession.claimed_by_user || '—')}
            +
            Ответов AI${escapeHtml(String(selectedSession.assistant_turns || 0))}
            +
            Сообщений клиента${escapeHtml(String(selectedSession.user_turns || 0))}
            +
            Вызовов инструментов${escapeHtml(String(selectedSession.tool_turns || 0))}
            +
            Средняя задержка${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_avg_ms))}
            +
            P95 задержки${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_p95_ms))}
            +
            Причина${escapeHtml(selectedSession.reason_label || selectedSession.raw_handoff_reason || '—')}
            +
            Исходная причина${escapeHtml(selectedSession.raw_handoff_reason || '—')}
            +
            Создано${escapeHtml(formatTime(selectedSession.created_at))}
            +
            Обновлено${escapeHtml(formatTime(selectedSession.updated_at))}
            +
            Закрыто${escapeHtml(formatTime(selectedSession.closed_at))}
            +
            +
            + ${linkedInteractionMarkup} +
            + ${timelineMarkup} +
            + `; +} + +async function exportAnalyticsDrilldownCsv() { + if (state.drilldown.mode !== 'ai') { + return exportAnalyticsDrilldownCsvBase(); + } + if (!state.drilldown.open || !state.drilldown.filters || !state.drilldown.total || state.drilldown.exporting) { + return; + } + state.drilldown.exporting = true; + state.drilldown.notice = ''; + renderAnalyticsDrilldown(); + + try { + const lines = [[ + 'session_id', + 'interaction_id', + 'thread_id', + 'channel', + 'queue_id', + 'status', + 'reason_key', + 'reason_label', + 'raw_handoff_reason', + 'assigned_to', + 'claimed_by_user', + 'assistant_turns', + 'user_turns', + 'tool_turns', + 'ai_latency_avg_ms', + 'ai_latency_p95_ms', + 'created_at', + 'updated_at', + 'closed_at', + ].join(',')]; + const maxRows = Math.min(state.drilldown.total, DRILLDOWN_EXPORT_SOFT_CAP); + let currentOffset = 0; + while (currentOffset < maxRows) { + const batchLimit = Math.min(DRILLDOWN_EXPORT_BATCH_SIZE, maxRows - currentOffset); + const payload = await fetchAiAnalyticsDrilldown(state.drilldown.filters, batchLimit, currentOffset); + const items = Array.isArray(payload?.items) ? payload.items : []; + items.forEach((item) => { + lines.push([ + item.session_id, + item.interaction_id, + item.thread_id, + item.channel, + item.queue_id, + item.status, + item.reason_key, + item.reason_label, + item.raw_handoff_reason, + item.assigned_to, + item.claimed_by_user, + item.assistant_turns, + item.user_turns, + item.tool_turns, + item.ai_latency_avg_ms, + item.ai_latency_p95_ms, + item.created_at, + item.updated_at, + item.closed_at, + ].map(csvCell).join(',')); + }); + if (!items.length) { + break; + } + currentOffset += batchLimit; + } + + const dateStamp = new Date().toISOString().slice(0, 10); + downloadCsv( + lines.join('\n'), + `konturcc-drilldown-${analyticsDrilldownFilenameSource()}-${dateStamp}.csv`, + ); + state.drilldown.notice = state.drilldown.total > DRILLDOWN_EXPORT_SOFT_CAP + ? `Экспортированы первые ${formatAnalyticsNumber(DRILLDOWN_EXPORT_SOFT_CAP, 0)} строк.` + : 'CSV готов.'; + } catch (err) { + state.drilldown.error = err.message; + } finally { + state.drilldown.exporting = false; + renderAnalyticsDrilldown(); + } +} + +function renderAiAnalyticsDrilldownActive() { + const drawer = $('analyticsDrilldownDrawer'); + const backdrop = $('analyticsDrilldownBackdrop'); + const closeBtn = $('analyticsDrilldownCloseBtn'); + const title = $('analyticsDrilldownTitle'); + const meta = $('analyticsDrilldownMeta'); + const filters = $('analyticsDrilldownFilters'); + const list = $('analyticsDrilldownList'); + const detail = $('analyticsDrilldownDetail'); + const prevBtn = $('analyticsDrilldownPrevBtn'); + const nextBtn = $('analyticsDrilldownNextBtn'); + const pageMeta = $('analyticsDrilldownPageMeta'); + const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong'); + const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong'); + const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint'); + if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) { + return; + } + + const isOpen = state.drilldown.open; + document.body.classList.toggle('analytics-drilldown-open', isOpen); + drawer.classList.toggle('is-open', isOpen); + backdrop.classList.toggle('is-open', isOpen); + drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + closeBtn.classList.add('analytics-drawer-close'); + closeBtn.setAttribute('aria-label', 'Закрыть детализацию'); + closeBtn.textContent = 'Закрыть · Esc'; + drawer.hidden = !isOpen; + backdrop.hidden = !isOpen; + if (!isOpen) { + title.textContent = 'Детализация AI-сессий'; + meta.textContent = 'Выберите карточку, исход, причину передачи или канал, чтобы открыть срез по AI-сессиям.'; + filters.innerHTML = ''; + list.innerHTML = ''; + detail.innerHTML = ''; + pageMeta.textContent = '0 из 0'; + prevBtn.disabled = true; + nextBtn.disabled = true; + if (listHeading) { + listHeading.textContent = 'Список AI-сессий'; + } + if (detailHeading) { + detailHeading.textContent = 'Детали AI-сессии'; + } + if (detailHint) { + detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.'; + } + syncAnalyticsDrilldownControls(); + return; + } + + const chips = analyticsDrilldownChips(); + const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0; + const currentPageEnd = state.drilldown.total + ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total) + : 0; + title.textContent = state.drilldown.sourceLabel || 'Детализация AI-сессий'; + if (listHeading) { + listHeading.textContent = 'Список AI-сессий'; + } + if (detailHeading) { + detailHeading.textContent = 'Детали AI-сессии'; + } + if (detailHint) { + detailHint.textContent = 'Только метаданные: без текста сообщений, AI-сводок, записей и транскриптов.'; + } + meta.textContent = state.drilldown.error + ? `Не удалось обновить детализацию AI: ${state.drilldown.error}` + : state.drilldown.exporting + ? 'Готовим CSV по текущему AI-срезу.' + : state.drilldown.loading + ? 'Загружаем AI-сессии и ленту метаданных...' + : `${formatAnalyticsNumber(state.drilldown.total, 0)} AI-сессий в выбранном срезе.`; + filters.innerHTML = [ + ...chips.map((chip) => `${escapeHtml(chip)}`), + state.drilldown.notice + ? `${escapeHtml(state.drilldown.notice)}` + : '', + ].join(''); + pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`; + prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0; + nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total; + syncAnalyticsDrilldownControls(); + + if (state.drilldown.loading && !state.drilldown.items.length) { + list.innerHTML = '
            Загружаем AI-сессии...
            '; + } else if (!state.drilldown.items.length) { + list.innerHTML = '
            По этому AI-срезу сессий пока нет.
            '; + } else { + list.innerHTML = state.drilldown.items + .map((item) => ` + + `) + .join(''); + } + + const selectedSession = state.drilldown.selectedInteraction + || state.drilldown.items.find((item) => item.session_id === state.drilldown.selectedInteractionId) + || null; + if (!selectedSession) { + detail.innerHTML = '
            Выберите AI-сессию в списке, чтобы увидеть детали на уровне метаданных.
            '; + return; + } + + const timelineMarkup = state.drilldown.detailLoading + ? '
            Загружаем ленту метаданных...
            ' + : state.drilldown.selectedTimeline.length + ? state.drilldown.selectedTimeline + .map((event) => { + const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata); + const metadataMarkup = Object.keys(safeMetadata).length + ? `
            ${escapeHtml(JSON.stringify(safeMetadata, null, 2))}
            ` + : '

            Только событие уровня метаданных без текста сообщений.

            '; + return ` +
            +
            + ${escapeHtml(event.label || event.event_type || 'событие')} + ${escapeHtml(formatTime(event.ts))} +
            + ${metadataMarkup} +
            + `; + }) + .join('') + : '
            Лента метаданных по этой AI-сессии пока пуста.
            '; + + const linkedInteractionMarkup = state.drilldown.selectedLinkedInteraction + ? ` +
            +
            +
            + Связанное обращение +

            ${escapeHtml(state.drilldown.selectedLinkedInteraction.interaction_id || '—')}

            +
            +
            +
            +
            Канал${escapeHtml(analyticsChannelLabel(state.drilldown.selectedLinkedInteraction.channel))}
            +
            Очередь${escapeHtml(analyticsQueueName(state.drilldown.selectedLinkedInteraction.queue_id))}
            +
            Статус${escapeHtml(interactionStatusLabel(state.drilldown.selectedLinkedInteraction.status))}
            +
            Назначен${escapeHtml(state.drilldown.selectedLinkedInteraction.assigned_to || 'Не назначен')}
            +
            Тема${escapeHtml(state.drilldown.selectedLinkedInteraction.subject || '—')}
            +
            Обновлено${escapeHtml(formatTime(state.drilldown.selectedLinkedInteraction.updated_at))}
            +
            +
            + ` + : ''; + + detail.innerHTML = ` +
            +
            +
            + ${escapeHtml(selectedSession.session_id)} +

            ${escapeHtml(aiAnalyticsSliceLabel(state.drilldown.filters?.slice || 'all'))}

            +
            +
            + ${escapeHtml(aiAnalyticsStatusLabel(selectedSession.status))} +
            +
            +
            +

            ${escapeHtml(aiAnalyticsDrilldownNote(state.drilldown.filters))}

            +
            + ${renderAiAnalyticsDrilldownFactBadges(selectedSession)} +
            +
            Канал${escapeHtml(analyticsChannelLabel(selectedSession.channel))}
            +
            Очередь${escapeHtml(analyticsQueueName(selectedSession.queue_id))}
            +
            ID обращения${escapeHtml(selectedSession.interaction_id || '—')}
            +
            ID диалога${escapeHtml(selectedSession.thread_id || '—')}
            +
            Назначено${escapeHtml(selectedSession.assigned_to || 'Не назначен')}
            +
            Взял оператор${escapeHtml(selectedSession.claimed_by_user || '—')}
            +
            Ответов AI${escapeHtml(String(selectedSession.assistant_turns || 0))}
            +
            Сообщений клиента${escapeHtml(String(selectedSession.user_turns || 0))}
            +
            Вызовов инструментов${escapeHtml(String(selectedSession.tool_turns || 0))}
            +
            Средняя задержка${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_avg_ms))}
            +
            P95 задержки${escapeHtml(formatAiAnalyticsMetric('ai_latency_avg_ms', selectedSession.ai_latency_p95_ms))}
            +
            Причина${escapeHtml(selectedSession.reason_label || selectedSession.raw_handoff_reason || '—')}
            +
            Исходная причина${escapeHtml(selectedSession.raw_handoff_reason || '—')}
            +
            Создано${escapeHtml(formatTime(selectedSession.created_at))}
            +
            Обновлено${escapeHtml(formatTime(selectedSession.updated_at))}
            +
            Закрыто${escapeHtml(formatTime(selectedSession.closed_at))}
            +
            +
            + ${linkedInteractionMarkup} +
            + ${timelineMarkup} +
            + `; +} + +function renderAnalyticsDrilldown() { + if (state.drilldown.mode === 'ai') { + renderAiAnalyticsDrilldownActive(); + return; + } + const drawer = $('analyticsDrilldownDrawer'); + const backdrop = $('analyticsDrilldownBackdrop'); + const closeBtn = $('analyticsDrilldownCloseBtn'); + const title = $('analyticsDrilldownTitle'); + const meta = $('analyticsDrilldownMeta'); + const filters = $('analyticsDrilldownFilters'); + const list = $('analyticsDrilldownList'); + const detail = $('analyticsDrilldownDetail'); + const prevBtn = $('analyticsDrilldownPrevBtn'); + const nextBtn = $('analyticsDrilldownNextBtn'); + const pageMeta = $('analyticsDrilldownPageMeta'); + const listHeading = drawer?.querySelector('.analytics-drawer-list-shell .analytics-drawer-list-head strong'); + const detailHeading = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head strong'); + const detailHint = drawer?.querySelector('.analytics-drawer-detail-shell .analytics-drawer-list-head .hint'); + if (!drawer || !backdrop || !closeBtn || !title || !meta || !filters || !list || !detail || !prevBtn || !nextBtn || !pageMeta) { + return; + } + + const isOpen = state.drilldown.open; + document.body.classList.toggle('analytics-drilldown-open', isOpen); + drawer.classList.toggle('is-open', isOpen); + backdrop.classList.toggle('is-open', isOpen); + drawer.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + backdrop.setAttribute('aria-hidden', isOpen ? 'false' : 'true'); + closeBtn.classList.add('analytics-drawer-close'); + closeBtn.setAttribute('aria-label', 'Закрыть детализацию'); + closeBtn.textContent = 'Закрыть · Esc'; + drawer.hidden = !isOpen; + backdrop.hidden = !isOpen; + if (!isOpen) { + title.textContent = 'Детализация обращений'; + meta.textContent = 'Выберите карточку обращений, канал или очередь на витрине, чтобы открыть список обращений.'; + filters.innerHTML = ''; + list.innerHTML = ''; + detail.innerHTML = ''; + pageMeta.textContent = '0 из 0'; + prevBtn.disabled = true; + nextBtn.disabled = true; + title.textContent = 'Точная детализация'; + meta.textContent = 'Выберите KPI-карточку, канал или очередь на витрине, чтобы открыть точный срез по обращениям.'; + pageMeta.textContent = '0 из 0'; + if (listHeading) { + listHeading.textContent = 'Список обращений'; + } + if (detailHeading) { + detailHeading.textContent = 'Карточка обращения'; + } + if (detailHint) { + detailHint.textContent = 'Лента событий и базовые поля без записей, транскриптов и канального контента.'; + } + syncAnalyticsDrilldownControls(); + return; + } + + const chips = analyticsDrilldownChips(); + const currentPageStart = state.drilldown.total ? state.drilldown.offset + 1 : 0; + const currentPageEnd = state.drilldown.total + ? Math.min(state.drilldown.offset + state.drilldown.items.length, state.drilldown.total) + : 0; + title.textContent = state.drilldown.sourceLabel || 'Точная детализация'; + if (listHeading) { + listHeading.textContent = state.drilldown.mode === 'metric' ? 'Точный KPI-срез' : 'Список обращений'; + } + if (detailHeading) { + detailHeading.textContent = state.drilldown.mode === 'metric' ? 'Карточка и KPI-факты' : 'Карточка обращения'; + } + if (detailHint) { + detailHint.textContent = state.drilldown.mode === 'metric' + ? 'KPI-факты, лента событий и базовые поля без исходных сообщений, записей, транскриптов и AI-сводок.' + : 'Лента событий и базовые поля без записей, транскриптов и канального контента.'; + } + title.textContent = state.drilldown.sourceLabel || 'Детализация обращений'; + meta.textContent = state.drilldown.error + ? `Не удалось полностью обновить детализацию: ${state.drilldown.error}` + : state.drilldown.exporting + ? 'Готовим CSV по текущему срезу. Детализация и фильтры остаются доступны только на уровне метаданных.' + : state.drilldown.loading + ? 'Загружаем список обращений и ленту событий...' + : `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в выбранном срезе.`; + meta.textContent = state.drilldown.error + ? `Не удалось обновить детализацию: ${state.drilldown.error}` + : state.drilldown.exporting + ? 'Готовим CSV по текущему срезу. Детализация остаётся только на уровне метаданных.' + : state.drilldown.loading + ? state.drilldown.mode === 'metric' + ? 'Загружаем точный KPI-срез и ленту событий...' + : 'Загружаем список обращений и ленту событий...' + : state.drilldown.mode === 'metric' + ? `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в точном срезе по метрике. ${state.drilldown.metricNote || ''}`.trim() + : `${formatAnalyticsNumber(state.drilldown.total, 0)} обращений в выбранном срезе.`; + title.textContent = state.drilldown.sourceLabel || 'Точная детализация'; + filters.innerHTML = [ + ...chips.map((chip) => `${escapeHtml(chip)}`), + state.drilldown.notice + ? `${escapeHtml(state.drilldown.notice)}` + : '', + ].join(''); + pageMeta.textContent = `${currentPageStart}-${currentPageEnd} из ${formatAnalyticsNumber(state.drilldown.total, 0)}`; + prevBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset <= 0; + nextBtn.disabled = state.drilldown.loading || state.drilldown.exporting || state.drilldown.offset + state.drilldown.limit >= state.drilldown.total; + syncAnalyticsDrilldownControls(); + + if (state.drilldown.loading && !state.drilldown.items.length) { + list.innerHTML = '
            Загружаем список обращений...
            '; + } else if (!state.drilldown.items.length) { + list.innerHTML = '
            По этому срезу обращений пока нет.
            '; + } else { + list.innerHTML = state.drilldown.items + .map((item) => ` + + `) + .join(''); + } + + const selectedInteraction = state.drilldown.selectedInteraction + || state.drilldown.items.find((item) => item.interaction_id === state.drilldown.selectedInteractionId) + || null; + if (!selectedInteraction) { + detail.innerHTML = '
            Выберите обращение в списке, чтобы увидеть карточку и timeline.
            '; + return; + } + + const timelineMarkup = state.drilldown.detailLoading + ? '
            Загружаем ленту событий...
            ' + : state.drilldown.selectedTimeline.length + ? state.drilldown.selectedTimeline + .map((event) => ` + ${(() => { + const safeMetadata = sanitizeAnalyticsTimelineMetadata(event.metadata); + const metadataMarkup = Object.keys(safeMetadata).length + ? `
            ${escapeHtml(JSON.stringify(safeMetadata, null, 2))}
            ` + : '

            Только событие уровня метаданных: исходный текст, записи и транскрипты скрыты в analyst-экране.

            '; + return ` +
            +
            + ${escapeHtml(event.action || 'событие')} + ${escapeHtml(formatTime(event.timestamp))} +
            + ${metadataMarkup} +
            + `; + })()} + `) + .join('') + : '
            Лента событий по этому обращению пока пуста.
            '; + + const metricContextMarkup = state.drilldown.mode === 'metric' && state.drilldown.metric + ? ` +
            +

            ${escapeHtml(state.drilldown.metricNote || '')}

            +
            + ` + : ''; + const factBadgesMarkup = renderAnalyticsDrilldownFactBadges(selectedInteraction); + const factFields = [ + selectedInteraction.wait_seconds !== null && selectedInteraction.wait_seconds !== undefined + ? `
            Ожидание${escapeHtml(`${formatAnalyticsNumber(selectedInteraction.wait_seconds, 0)} с`)}
            ` + : '', + selectedInteraction.handle_seconds !== null && selectedInteraction.handle_seconds !== undefined + ? `
            Обработка${escapeHtml(`${formatAnalyticsNumber(selectedInteraction.handle_seconds, 0)} с`)}
            ` + : '', + selectedInteraction.within_sla !== null && selectedInteraction.within_sla !== undefined + ? `
            SLA${escapeHtml(selectedInteraction.within_sla ? 'В SLA' : 'Вне SLA')}
            ` + : '', + selectedInteraction.resolved_first_contact !== null && selectedInteraction.resolved_first_contact !== undefined + ? `
            FCR${escapeHtml(selectedInteraction.resolved_first_contact ? 'Да' : 'Нет')}
            ` + : '', + selectedInteraction.abandoned !== null && selectedInteraction.abandoned !== undefined + ? `
            Потеря${escapeHtml(selectedInteraction.abandoned ? 'Да' : 'Нет')}
            ` + : '', + ].filter(Boolean).join(''); + + detail.innerHTML = ` +
            +
            +
            + ${escapeHtml(selectedInteraction.subject || selectedInteraction.interaction_id)} +

            ${escapeHtml(selectedInteraction.interaction_id)}

            +
            +
            + ${state.drilldown.mode === 'metric' && state.drilldown.metric ? analyticsMetricCoverageBadge(state.drilldown.metric) : ''} + ${escapeHtml(interactionStatusLabel(selectedInteraction.status))} +
            +
            + ${metricContextMarkup} + ${factBadgesMarkup} +
            +
            Канал${escapeHtml(analyticsChannelLabel(selectedInteraction.channel))}
            +
            Очередь${escapeHtml(analyticsQueueName(selectedInteraction.queue_id))}
            +
            Назначен${escapeHtml(selectedInteraction.assigned_to || 'Не назначен')}
            +
            Создано${escapeHtml(formatTime(selectedInteraction.created_at))}
            +
            Обновлено${escapeHtml(formatTime(selectedInteraction.updated_at))}
            +
            Срез${escapeHtml(state.drilldown.sourceLabel || 'Все обращения')}
            + ${factFields} +
            +
            +
            + ${timelineMarkup} +
            + `; +} + +async function loadAiAnalyticsDrilldownDetailsState(sessionId) { + if (!state.drilldown.open || !sessionId) { + return; + } + state.drilldown.selectedInteractionId = sessionId; + state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.session_id === sessionId) || null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.error = ''; + state.drilldown.detailLoading = true; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + + if (analyticsMockEnabled()) { + await ensureAnalyticsMockDataLoaded(); + const detail = buildMockAiDetail(sessionId); + state.drilldown.selectedInteraction = { + ...(state.drilldown.selectedInteraction || {}), + ...(detail?.session || {}), + }; + state.drilldown.selectedLinkedInteraction = detail?.interaction || null; + state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : []; + state.drilldown.detailLoading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + return; + } + + try { + const detail = await fetchAiAnalyticsSessionDetail(sessionId); + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== sessionId) { + return; + } + state.drilldown.selectedInteraction = { + ...(state.drilldown.selectedInteraction || {}), + ...(detail?.session || {}), + }; + state.drilldown.selectedLinkedInteraction = detail?.interaction || null; + state.drilldown.selectedTimeline = Array.isArray(detail?.timeline) ? detail.timeline : []; + } catch (err) { + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== sessionId) { + return; + } + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open && state.drilldown.selectedInteractionId === sessionId) { + state.drilldown.detailLoading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + } + } +} + +loadAnalyticsDrilldownDetails = async function loadAnalyticsDrilldownDetails(interactionId) { + if (state.drilldown.mode === 'ai') { + return loadAiAnalyticsDrilldownDetailsState(interactionId); + } + if (!state.drilldown.open || !interactionId) { + return; + } + state.drilldown.selectedInteractionId = interactionId; + state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.interaction_id === interactionId) || null; + state.drilldown.selectedTimeline = []; + state.drilldown.error = ''; + state.drilldown.detailLoading = true; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + + if (analyticsMockEnabled()) { + await ensureAnalyticsMockDataLoaded(); + const detail = buildMockInteractionDetail(interactionId); + state.drilldown.selectedInteraction = { + ...(state.drilldown.selectedInteraction || {}), + ...(detail?.detail || {}), + }; + state.drilldown.selectedTimeline = Array.isArray(detail?.timeline?.events) ? detail.timeline.events : []; + state.drilldown.detailLoading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + return; + } + + try { + const [detail, timeline] = await Promise.all([ + api('interaction', `interactions/${encodeURIComponent(interactionId)}`), + api('interaction', `interactions/${encodeURIComponent(interactionId)}/timeline`), + ]); + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) { + return; + } + state.drilldown.selectedInteraction = { + ...(state.drilldown.selectedInteraction || {}), + ...(detail || {}), + }; + state.drilldown.selectedTimeline = Array.isArray(timeline?.events) ? timeline.events : []; + } catch (err) { + if (!state.drilldown.open || state.drilldown.selectedInteractionId !== interactionId) { + return; + } + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open && state.drilldown.selectedInteractionId === interactionId) { + state.drilldown.detailLoading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + } + } +} + +async function loadAiAnalyticsDrilldownPageState(options = {}) { + if (!state.drilldown.open || !state.drilldown.filters) { + return; + } + const preserveSelection = Boolean(options.preserveSelection); + state.drilldown.loading = true; + state.drilldown.error = ''; + renderAnalyticsDrilldown(); + + if (analyticsMockEnabled()) { + await ensureAnalyticsMockDataLoaded(); + const data = buildMockAiDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset); + state.drilldown.items = Array.isArray(data?.items) ? data.items : []; + state.drilldown.total = Number(data?.total || 0); + state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE); + state.drilldown.offset = Number(data?.offset || 0); + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters); + state.drilldown.coverage = data?.coverage || state.drilldown.coverage; + state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters); + const selectedId = state.drilldown.selectedInteractionId || ''; + const selectedStillVisible = preserveSelection + && selectedId + && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId); + if (selectedStillVisible) { + state.drilldown.selectedInteraction = state.drilldown.items.find((item) => analyticsDrilldownCurrentItemId(item) === selectedId) || state.drilldown.selectedInteraction; + void loadAnalyticsDrilldownDetails(selectedId); + } else if (state.drilldown.items.length) { + state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]); + state.drilldown.selectedInteraction = state.drilldown.items[0]; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else { + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + } + state.drilldown.loading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + return; + } + + try { + const data = await fetchAiAnalyticsDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset); + if (!state.drilldown.open) { + return; + } + state.drilldown.items = Array.isArray(data?.items) ? data.items : []; + state.drilldown.total = Number(data?.total || 0); + state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE); + state.drilldown.offset = Number(data?.offset || 0); + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters); + state.drilldown.coverage = data?.coverage || state.drilldown.coverage; + state.drilldown.metricNote = aiAnalyticsDrilldownNote(state.drilldown.filters); + + const selectedId = state.drilldown.selectedInteractionId || ''; + const selectedStillVisible = preserveSelection + && selectedId + && state.drilldown.items.some((item) => analyticsDrilldownCurrentItemId(item) === selectedId); + if (selectedStillVisible) { + state.drilldown.selectedInteraction = state.drilldown.items.find( + (item) => analyticsDrilldownCurrentItemId(item) === selectedId, + ) || state.drilldown.selectedInteraction; + void loadAnalyticsDrilldownDetails(selectedId); + } else if (state.drilldown.items.length) { + state.drilldown.selectedInteractionId = analyticsDrilldownCurrentItemId(state.drilldown.items[0]); + state.drilldown.selectedInteraction = state.drilldown.items[0]; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else { + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + } + } catch (err) { + if (!state.drilldown.open) { + return; + } + state.drilldown.items = []; + state.drilldown.total = 0; + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedLinkedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open) { + state.drilldown.loading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + } + } +} + +loadAnalyticsDrilldownPage = async function loadAnalyticsDrilldownPage(options = {}) { + if (state.drilldown.mode === 'ai') { + return loadAiAnalyticsDrilldownPageState(options); + } + if (!state.drilldown.open || !state.drilldown.filters) { + return; + } + const preserveSelection = Boolean(options.preserveSelection); + state.drilldown.loading = true; + state.drilldown.error = ''; + renderAnalyticsDrilldown(); + + if (analyticsMockEnabled()) { + await ensureAnalyticsMockDataLoaded(); + const data = buildMockInteractionDrilldownData(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset, state.drilldown.mode); + state.drilldown.items = Array.isArray(data?.items) ? data.items : []; + state.drilldown.total = Number(data?.total || 0); + state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE); + state.drilldown.offset = Number(data?.offset || 0); + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters); + state.drilldown.metric = data?.metric || state.drilldown.metric; + state.drilldown.coverage = data?.coverage || state.drilldown.coverage; + if (state.drilldown.mode === 'metric' && state.drilldown.metric) { + state.drilldown.metricNote = analyticsMetricDrilldownNote(state.drilldown.metric); + } + const selectedStillVisible = preserveSelection + && state.drilldown.selectedInteractionId + && state.drilldown.items.some((item) => item.interaction_id === state.drilldown.selectedInteractionId); + if (selectedStillVisible) { + state.drilldown.selectedInteraction = state.drilldown.items.find((item) => item.interaction_id === state.drilldown.selectedInteractionId) || state.drilldown.selectedInteraction; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else if (state.drilldown.items.length) { + state.drilldown.selectedInteractionId = state.drilldown.items[0].interaction_id; + state.drilldown.selectedInteraction = state.drilldown.items[0]; + state.drilldown.selectedTimeline = []; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else { + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + } + state.drilldown.loading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + return; + } + + try { + const data = state.drilldown.mode === 'metric' + ? await fetchAnalyticsMetricDrilldown(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset) + : await api( + 'interaction', + `interactions/drilldown?${analyticsDrilldownQuery(state.drilldown.filters, state.drilldown.limit, state.drilldown.offset)}`, + ); + if (!state.drilldown.open) { + return; + } + state.drilldown.items = Array.isArray(data?.items) ? data.items : []; + state.drilldown.total = Number(data?.total || 0); + state.drilldown.limit = Number(data?.limit || state.drilldown.limit || DRILLDOWN_PAGE_SIZE); + state.drilldown.offset = Number(data?.offset || 0); + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(data?.filters || state.drilldown.filters); + state.drilldown.metric = data?.metric || state.drilldown.metric; + state.drilldown.coverage = data?.coverage || state.drilldown.coverage; + if (state.drilldown.mode === 'metric' && state.drilldown.metric) { + state.drilldown.metricNote = analyticsMetricDrilldownNote(state.drilldown.metric); + } + + const selectedStillVisible = preserveSelection + && state.drilldown.selectedInteractionId + && state.drilldown.items.some((item) => item.interaction_id === state.drilldown.selectedInteractionId); + if (selectedStillVisible) { + state.drilldown.selectedInteraction = state.drilldown.items.find( + (item) => item.interaction_id === state.drilldown.selectedInteractionId, + ) || state.drilldown.selectedInteraction; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else if (state.drilldown.items.length) { + state.drilldown.selectedInteractionId = state.drilldown.items[0].interaction_id; + state.drilldown.selectedInteraction = state.drilldown.items[0]; + state.drilldown.selectedTimeline = []; + void loadAnalyticsDrilldownDetails(state.drilldown.selectedInteractionId); + } else { + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + } + } catch (err) { + if (!state.drilldown.open) { + return; + } + state.drilldown.items = []; + state.drilldown.total = 0; + state.drilldown.selectedInteractionId = ''; + state.drilldown.selectedInteraction = null; + state.drilldown.selectedTimeline = []; + state.drilldown.detailLoading = false; + state.drilldown.error = err.message; + } finally { + if (state.drilldown.open) { + state.drilldown.loading = false; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + } + } +} + +openAnalyticsDrilldown = function openAnalyticsDrilldown(sourceType, sourceValue = '', options = {}) { + const mode = options.mode || (sourceType === 'metric' ? 'metric' : 'interaction'); + const filters = analyticsDrilldownBaseFilters(); + if (mode === 'metric') { + filters.metric = options.metric || sourceValue; + filters.status = ''; + filters.q = ''; + } + if (mode === 'ai') { + filters.slice = options.slice || 'all'; + filters.reason_key = options.reasonKey || ''; + filters.status = ''; + filters.q = ''; + filters.sort_by = 'created_at'; + filters.sort_dir = 'desc'; + if (sourceType === 'ai-overview' && sourceValue) { + filters.slice = sourceValue; + } + if (sourceType === 'ai-channel' && sourceValue) { + filters.channel = sourceValue; + } + if (sourceType === 'ai-reason' && sourceValue) { + filters.slice = 'handoff'; + filters.reason_key = options.reasonKey || sourceValue; + } + } + if (sourceType === 'channel' && sourceValue) { + filters.channel = sourceValue; + } + if (sourceType === 'queue' && sourceValue) { + filters.queue_id = sourceValue; + } + if (sourceType === 'agent' && sourceValue) { + filters.agent_id = sourceValue; + } + rememberAnalyticsDrilldownReturnFocus(); + state.drilldown = { + ...emptyDrilldownState(), + open: true, + mode, + sourceType, + sourceLabel: options.sourceLabel || analyticsDrilldownSourceLabel(sourceType, sourceValue), + sourceValue, + metric: mode === 'metric' ? (options.metric || sourceValue) : '', + filters: normalizeAnalyticsDrilldownFilters(filters), + coverage: options.coverage || null, + metricNote: mode === 'metric' + ? (options.metricNote || analyticsMetricDrilldownNote(options.metric || sourceValue)) + : mode === 'ai' + ? aiAnalyticsDrilldownNote(filters) + : '', + }; + renderAnalyticsDrilldown(); + syncAnalyticsUrlState(); + window.requestAnimationFrame(() => { + focusAnalyticsDrilldownPrimaryControl(true); + }); + void loadAnalyticsDrilldownPage(); +} + +function renderAnalyticsEmptyState() { + const box = $('analyticsEmptyState'); + if (state.analytics.error) { + box.hidden = false; + box.textContent = `Не удалось обновить витрину: ${state.analytics.error}`; + return; + } + const total = Number(state.analytics.overview?.volume?.total || 0); + if (!state.analytics.loading && total === 0) { + box.hidden = false; + box.textContent = 'За выбранный период обращений пока нет. Измените период, очередь или канал, чтобы увидеть данные.'; + return; + } + box.hidden = true; + box.textContent = ''; +} + +function renderAnalyticsDashboard(rangeMeta = analyticsRangeFromControls()) { + const refreshBtn = $('refreshBtn'); + const analyticsRefreshBtn = $('analyticsRefreshBtn'); + const analyticsExportBtn = $('analyticsExportBtn'); + const analyticsSaveViewBtn = $('analyticsSaveViewBtn'); + if (refreshBtn) { + refreshBtn.disabled = state.analytics.loading; + } + if (analyticsRefreshBtn) { + analyticsRefreshBtn.disabled = state.analytics.loading; + } + if (analyticsExportBtn) { + analyticsExportBtn.disabled = state.analytics.loading; + } + if (analyticsSaveViewBtn) { + analyticsSaveViewBtn.disabled = state.analytics.loading; + } + renderAnalyticsQueueOptions(); + renderSavedAnalyticsViews(); + updateAnalyticsRangeHint(rangeMeta); + renderAnalyticsNarrative(rangeMeta); + renderAnalyticsComparePanel(); + renderAnalyticsOverview(); + renderVoiceNameAnalyticsEmptyState(); + renderVoiceNameAnalyticsOverview(); + renderVoiceNameAnalyticsTrendChart(); + renderVoiceNameAnalyticsFunnel(); + renderVoiceNameAnalyticsLanguageTable(); + renderVoiceNameAnalyticsQueueTable(); + renderVoiceNameAnalyticsHandoffTable(); + renderAiAnalyticsEmptyState(); + renderAiAnalyticsOverview(); + renderAiAnalyticsTrendChart(); + renderAiAnalyticsChannelComparison(); + renderAiAnalyticsCoverage(); + renderAgentAnalyticsEmptyState(); + renderAgentAnalyticsOverview(); + renderAgentAnalyticsStateStrip(); + renderAgentAnalyticsTrendChart(); + renderAgentAnalyticsTeamTable(); + renderAgentAnalyticsShiftTable(); + renderAgentAnalyticsTable(); + renderAnalyticsTrendChart(); + renderAnalyticsChannelTable(); + renderAnalyticsQueueTable(); + renderAnalyticsCoverage(); + renderAnalyticsEmptyState(); + renderAnalyticsDrilldown(); +} + +async function loadAnalyticsDashboard(_silent = false) { + syncAnalyticsStateFromControls(); + const rangeMeta = analyticsRangeFromControls(); + const requestId = state.analytics.requestId + 1; + state.analytics.requestId = requestId; + state.analytics.lastRangeMeta = rangeMeta; + state.analytics.loading = true; + state.analytics.error = ''; + state.analytics.voiceNameError = ''; + state.analytics.voiceNameTrendError = ''; + state.analytics.aiError = ''; + state.analytics.agentError = ''; + state.analytics.agentTrendError = ''; + renderAnalyticsDashboard(rangeMeta); + + if (analyticsMockEnabled()) { + await ensureAnalyticsMockDataLoaded(); + const payload = buildMockAnalyticsDashboard(rangeMeta); + if (requestId !== state.analytics.requestId) { + return; + } + state.analytics.queueOptions = payload.queueOptions; + state.analytics.queueAccessError = ''; + state.analytics.overview = payload.overview; + state.analytics.compare = state.analytics.compareMode === 'previous' ? payload.compare : emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString()); + state.analytics.coverage = payload.coverage; + state.analytics.voiceNameOverview = payload.voiceNameOverview; + state.analytics.voiceNameCompare = state.analytics.compareMode === 'previous' + ? payload.voiceNameCompare + : emptyVoiceNameAnalyticsOverview( + rangeMeta.previous.from.toISOString(), + rangeMeta.previous.to.toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + state.analytics.aiOverview = payload.aiOverview; + state.analytics.aiCompare = state.analytics.compareMode === 'previous' ? payload.aiCompare : emptyAiAnalyticsOverview(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString(), state.analytics.channel, state.analytics.queueId === 'all' ? null : state.analytics.queueId); + state.analytics.agentOverview = payload.agentOverview; + state.analytics.agentCompare = state.analytics.compareMode === 'previous' ? payload.agentCompare : emptyAgentAnalyticsOverview(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString(), state.analytics.channel, state.analytics.queueId === 'all' ? null : state.analytics.queueId); + state.analytics.agentRows = Array.isArray(payload.agentOverview?.items) ? payload.agentOverview.items : []; + state.analytics.channelRows = payload.channelRows; + state.analytics.queueRows = payload.queueRows; + state.analytics.trend = payload.trend; + state.analytics.voiceNameTrend = payload.voiceNameTrend; + state.analytics.aiTrend = payload.aiTrend; + state.analytics.agentTrend = payload.agentTrend; + state.analytics.loading = false; + state.analytics.statusMessage = state.analytics.mockDataError + ? `Показаны демонстрационные данные. Не удалось загрузить внешний mock-файл: ${state.analytics.mockDataError}` + : 'Показаны демонстрационные данные из внешнего mock-файла.'; + renderAnalyticsDashboard(rangeMeta); + syncAnalyticsUrlState(); + if (state.drilldown.open) { + void loadAnalyticsDrilldownPage({ preserveSelection: true }); + } + return; + } + + try { + const queuePromise = loadAnalyticsQueueOptions(); + const previousWindowPromise = state.analytics.compareMode === 'previous' + ? fetchAnalyticsKpi(rangeMeta.previous).catch(() => emptyKpiEnvelope()) + : Promise.resolve(emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString())); + const emptyAiCurrent = emptyAiAnalyticsOverview( + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + aiAnalyticsSupportedChannel() ? state.analytics.channel : state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + const emptyAiPrevious = emptyAiAnalyticsOverview( + rangeMeta.previous.from.toISOString(), + rangeMeta.previous.to.toISOString(), + aiAnalyticsSupportedChannel() ? state.analytics.channel : state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + const emptyVoiceCurrent = emptyVoiceNameAnalyticsOverview( + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + const emptyVoicePrevious = emptyVoiceNameAnalyticsOverview( + rangeMeta.previous.from.toISOString(), + rangeMeta.previous.to.toISOString(), + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + null, + ); + const emptyAgentCurrent = emptyAgentAnalyticsOverview( + rangeMeta.current.from.toISOString(), + rangeMeta.current.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + const emptyAgentPrevious = emptyAgentAnalyticsOverview( + rangeMeta.previous.from.toISOString(), + rangeMeta.previous.to.toISOString(), + state.analytics.channel, + state.analytics.queueId === 'all' ? null : state.analytics.queueId, + ); + const aiOverviewPromise = aiAnalyticsSupportedChannel() + ? fetchAiAnalyticsOverview(rangeMeta.current).catch((err) => { + state.analytics.aiError = err.message; + return emptyAiCurrent; + }) + : Promise.resolve(emptyAiCurrent); + const aiComparePromise = aiAnalyticsSupportedChannel() && state.analytics.compareMode === 'previous' + ? fetchAiAnalyticsOverview(rangeMeta.previous).catch(() => emptyAiPrevious) + : Promise.resolve(emptyAiPrevious); + const voiceNameOverviewPromise = voiceNameAnalyticsSupportedChannel() + ? fetchVoiceNameAnalyticsOverview(rangeMeta.current).catch((err) => { + state.analytics.voiceNameError = err.message; + return emptyVoiceCurrent; + }) + : Promise.resolve(emptyVoiceCurrent); + const voiceNameComparePromise = voiceNameAnalyticsSupportedChannel() && state.analytics.compareMode === 'previous' + ? fetchVoiceNameAnalyticsOverview(rangeMeta.previous).catch(() => emptyVoicePrevious) + : Promise.resolve(emptyVoicePrevious); + const agentOverviewPromise = fetchAgentAnalyticsOverview(rangeMeta.current).catch((err) => { + state.analytics.agentError = err.message; + return emptyAgentCurrent; + }); + const agentComparePromise = state.analytics.compareMode === 'previous' + ? fetchAgentAnalyticsOverview(rangeMeta.previous).catch(() => emptyAgentPrevious) + : Promise.resolve(emptyAgentPrevious); + const [current, previous, coverage, queues, voiceNameOverview, voiceNameCompare, aiOverview, aiCompare, agentOverview, agentCompare] = await Promise.all([ + fetchAnalyticsKpi(rangeMeta.current), + previousWindowPromise, + api('reporting', 'reports/coverage').catch(() => ({ implemented_metrics: [], dimensions: [], supported_filters: [] })), + queuePromise, + voiceNameOverviewPromise, + voiceNameComparePromise, + aiOverviewPromise, + aiComparePromise, + agentOverviewPromise, + agentComparePromise, + ]); + + if (requestId !== state.analytics.requestId) { + return; + } + + state.analytics.overview = current || emptyKpiEnvelope(rangeMeta.current.from.toISOString(), rangeMeta.current.to.toISOString()); + state.analytics.compare = previous || emptyKpiEnvelope(rangeMeta.previous.from.toISOString(), rangeMeta.previous.to.toISOString()); + state.analytics.coverage = coverage || { implemented_metrics: [], dimensions: [], supported_filters: [] }; + state.analytics.voiceNameOverview = voiceNameOverview || emptyVoiceCurrent; + state.analytics.voiceNameCompare = voiceNameCompare || emptyVoicePrevious; + state.analytics.aiOverview = aiOverview || emptyAiCurrent; + state.analytics.aiCompare = aiCompare || emptyAiPrevious; + state.analytics.agentOverview = agentOverview || emptyAgentCurrent; + state.analytics.agentCompare = agentCompare || emptyAgentPrevious; + state.analytics.agentRows = Array.isArray(agentOverview?.items) ? agentOverview.items : []; + + const byChannel = current?.breakdowns?.by_channel || {}; + state.analytics.channelRows = Object.entries(byChannel) + .map(([channel, payload]) => ({ + channel, + total: Number(payload?.total || 0), + answered: Number(payload?.answered || 0), + abandoned: Number(payload?.abandoned || 0), + })) + .sort((a, b) => b.total - a.total || a.channel.localeCompare(b.channel)); + + const [trend, queueRows, voiceNameTrend, aiTrend, agentTrend] = await Promise.all([ + loadAnalyticsTrend(rangeMeta, requestId), + loadAnalyticsQueueRows(rangeMeta, queues, requestId), + loadVoiceNameAnalyticsTrend(rangeMeta, requestId), + loadAiAnalyticsTrend(rangeMeta, requestId), + loadAgentAnalyticsTrend(rangeMeta, requestId), + ]); + + if (requestId !== state.analytics.requestId) { + return; + } + + state.analytics.trend = trend; + state.analytics.queueRows = queueRows; + state.analytics.voiceNameTrend = voiceNameTrend; + state.analytics.aiTrend = aiTrend; + state.analytics.agentTrend = agentTrend; + state.analytics.loading = false; + renderAnalyticsDashboard(rangeMeta); + syncAnalyticsUrlState(); + if (state.drilldown.open) { + void loadAnalyticsDrilldownPage({ preserveSelection: true }); + } + } catch (err) { + if (requestId !== state.analytics.requestId) { + return; + } + state.analytics.loading = false; + state.analytics.error = err.message; + state.analytics.overview = emptyKpiEnvelope(); + state.analytics.compare = emptyKpiEnvelope(); + state.analytics.voiceNameOverview = emptyVoiceNameAnalyticsOverview(); + state.analytics.voiceNameCompare = emptyVoiceNameAnalyticsOverview(); + state.analytics.aiOverview = emptyAiAnalyticsOverview(); + state.analytics.aiCompare = emptyAiAnalyticsOverview(); + state.analytics.agentOverview = emptyAgentAnalyticsOverview(); + state.analytics.agentCompare = emptyAgentAnalyticsOverview(); + state.analytics.coverage = { implemented_metrics: [], dimensions: [], supported_filters: [] }; + state.analytics.trend = []; + state.analytics.voiceNameTrend = emptyVoiceNameAnalyticsTimeseries(state.analytics.voiceNameTrendMetric || 'scenario_calls'); + state.analytics.aiTrend = emptyAiAnalyticsTimeseries(state.analytics.aiTrendMetric || 'containment_rate'); + state.analytics.agentTrend = emptyAgentAnalyticsTimeseries(state.analytics.agentTrendMetric || 'interactions_per_agent'); + state.analytics.channelRows = []; + state.analytics.queueRows = []; + state.analytics.agentRows = []; + renderAnalyticsDashboard(rangeMeta); + syncAnalyticsUrlState(); + } +} + +function handleAnalyticsOverviewClick(event) { + const button = event.target.closest('[data-analytics-drilldown-source]'); + if (!button) { + return; + } + const metric = button.dataset.analyticsDrilldownMetric || ''; + const sourceType = button.dataset.analyticsDrilldownSource || 'overview'; + if (metric) { + openAnalyticsDrilldown('metric', metric, { + mode: 'metric', + metric, + sourceLabel: button.dataset.analyticsDrilldownLabel || analyticsDrilldownSourceLabel('metric', metric), + coverage: analyticsMetricCoverage(metric), + metricNote: analyticsMetricDrilldownNote(metric), + }); + return; + } + openAnalyticsDrilldown(sourceType); +} + +function handleAnalyticsChannelClick(event) { + const button = event.target.closest('[data-analytics-channel]'); + if (!button) { + return; + } + openAnalyticsDrilldown('channel', button.dataset.analyticsChannel || ''); +} + +function handleAnalyticsQueueClick(event) { + const button = event.target.closest('[data-analytics-queue-id]'); + if (!button) { + return; + } + openAnalyticsDrilldown('queue', button.dataset.analyticsQueueId || ''); +} + +function handleAgentAnalyticsClick(event) { + const button = event.target.closest('[data-analytics-agent-id]'); + if (!button) { + return; + } + openAnalyticsDrilldown('agent', button.dataset.analyticsAgentId || '', { + sourceLabel: button.dataset.analyticsSourceLabel || `Агент: ${button.dataset.analyticsAgentId || ''}`, + }); +} + +handleAnalyticsDrilldownListClick = function handleAnalyticsDrilldownListClick(event) { + if (state.drilldown.mode === 'ai') { + const button = event.target.closest('[data-analytics-row-id]'); + if (!button) { + return; + } + maybeScrollAnalyticsDrilldownDetailIntoView(); + void loadAnalyticsDrilldownDetails(button.dataset.analyticsRowId || ''); + return; + } + const button = event.target.closest('[data-analytics-interaction-id]'); + if (!button) { + return; + } + maybeScrollAnalyticsDrilldownDetailIntoView(); + void loadAnalyticsDrilldownDetails(button.dataset.analyticsInteractionId || ''); +} + +function handleAnalyticsDrilldownPrevPage() { + if (!state.drilldown.open || state.drilldown.offset <= 0 || state.drilldown.loading) { + return; + } + state.drilldown.offset = Math.max(0, state.drilldown.offset - state.drilldown.limit); + void loadAnalyticsDrilldownPage(); +} + +function handleAnalyticsDrilldownNextPage() { + if (!state.drilldown.open || state.drilldown.loading) { + return; + } + if (state.drilldown.offset + state.drilldown.limit >= state.drilldown.total) { + return; + } + state.drilldown.offset += state.drilldown.limit; + void loadAnalyticsDrilldownPage(); +} + +function handleAnalyticsFilterChange() { + closeAnalyticsDrilldown({ skipUrlSync: true }); + syncAnalyticsStateFromControls(); + detachActiveAnalyticsView(); + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + void loadAnalyticsDashboard(); +} + +function handleAnalyticsPresetChange() { + state.analytics.fromTs = ''; + state.analytics.toTs = ''; + $('analyticsFrom').value = ''; + $('analyticsTo').value = ''; + handleAnalyticsFilterChange(); +} + +function applySelectedAnalyticsView() { + const viewId = $('analyticsSavedViewSelect').value || ''; + if (!viewId) { + state.analytics.activeViewId = ''; + state.analytics.statusMessage = ''; + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + return; + } + const view = state.analytics.savedViews.find((item) => item.id === viewId); + if (!view) { + state.analytics.activeViewId = ''; + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + return; + } + state.analytics.activeViewId = view.id; + state.analytics.statusMessage = `Загружен вид «${view.name}».`; + applyAnalyticsSnapshot(view.snapshot); + closeAnalyticsDrilldown({ skipUrlSync: true }); + syncAnalyticsControlsFromState(); + syncAnalyticsUrlState(); + void loadAnalyticsDashboard(); +} + +function aiAnalyticsOutcomeSessions(payload, outcome) { + const rows = Array.isArray(payload?.breakdowns?.by_outcome) ? payload.breakdowns.by_outcome : []; + return Number(rows.find((item) => item.outcome === outcome)?.sessions || 0); +} + +function aiAnalyticsCanDrilldown(metric, payload = state.analytics.aiOverview || emptyAiAnalyticsOverview()) { + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + return false; + } + if (!['containment_rate', 'handoff_rate', 'human_touched_rate', 'closed_without_operator_rate'].includes(metric)) { + return false; + } + const total = Number(payload?.totals?.sessions_started || 0); + if (!total) { + return false; + } + return aiAnalyticsOutcomeSessions(payload, aiAnalyticsMetricSlice(metric)) > 0; +} + +function aiAnalyticsMetricDetail(metric, payload) { + const totals = payload?.totals || {}; + if (metric === 'containment_rate') { + return `${formatAnalyticsNumber(totals.sessions_contained || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`; + } + if (metric === 'handoff_rate') { + return `${formatAnalyticsNumber(totals.sessions_handoff || 0, 0)} из ${formatAnalyticsNumber(totals.sessions_started || 0, 0)} сессий`; + } + if (metric === 'human_touched_rate') { + return `${formatAnalyticsNumber(aiAnalyticsOutcomeSessions(payload, 'human_touched'), 0)} сессий с участием оператора`; + } + if (metric === 'ai_latency_avg_ms') { + return `${formatAnalyticsNumber(totals.assistant_turns || 0, 0)} ответов AI`; + } + if (metric === 'closed_without_operator_rate') { + return `${formatAnalyticsNumber(totals.sessions_closed_without_operator || 0, 0)} закрытий без оператора`; + } + return ''; +} + +function renderAiAnalyticsCard(label, metric, currentPayload, previousPayload) { + const value = aiAnalyticsMetricValue(metric, currentPayload); + const showCompare = state.analytics.compareMode === 'previous'; + const delta = showCompare ? aiAnalyticsDelta(metric, currentPayload, previousPayload) : null; + const note = AI_ANALYTICS_METRIC_META[metric]?.note || ''; + const detail = aiAnalyticsMetricDetail(metric, currentPayload); + const drilldownEnabled = aiAnalyticsCanDrilldown(metric, currentPayload); + const slice = aiAnalyticsMetricSlice(metric); + const tag = drilldownEnabled ? 'button' : 'div'; + const classes = [ + 'summary-card', + 'analytics-card', + 'ai-analytics-card', + drilldownEnabled ? 'analytics-card-button' : 'analytics-card-readonly', + ].join(' '); + const attributes = drilldownEnabled + ? `type="button" data-ai-drilldown-slice="${escapeHtml(slice)}" data-ai-drilldown-label="${escapeHtml(label)}"` + : ''; + return ` + <${tag} class="${classes}" ${attributes}> +
            +
            ${label}
            + ${showCompare ? `${delta.text}` : ''} +
            +
            ${formatAiAnalyticsMetric(metric, value)}
            + ${detail ? `
            ${detail}
            ` : ''} + ${note ? `
            ${note}
            ` : ''} + + `; +} + +function renderAiAnalyticsOverview() { + const box = $('aiAnalyticsOverview'); + if (!box) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + box.innerHTML = ''; + return; + } + const current = state.analytics.aiOverview || emptyAiAnalyticsOverview(); + if (!state.analytics.loading && Number(current?.totals?.sessions_started || 0) === 0) { + box.innerHTML = ''; + return; + } + const previous = state.analytics.aiCompare || emptyAiAnalyticsOverview(); + box.innerHTML = [ + renderAiAnalyticsCard('Закрыто AI', 'containment_rate', current, previous), + renderAiAnalyticsCard('Передано оператору', 'handoff_rate', current, previous), + renderAiAnalyticsCard('С участием оператора', 'human_touched_rate', current, previous), + renderAiAnalyticsCard('Закрыто без оператора', 'closed_without_operator_rate', current, previous), + ].join(''); +} + +function renderAiAnalyticsChannelComparison() { + const container = $('aiAnalyticsChannelComparison'); + const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_channel) + ? state.analytics.aiOverview.breakdowns.by_channel + : []; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!rows.length) { + container.innerHTML = '
            Нет AI-сессий по Telegram и WhatsApp за выбранный период.
            '; + return; + } + + container.innerHTML = ` +
            + Канал + Сессии + Только AI + С оператором + Передача + Средняя задержка +
            + ${rows + .map((item) => { + const aiOnlyShare = item.sessions_started ? (item.ai_only_sessions / item.sessions_started) * 100 : 0; + const humanTouchedShare = item.sessions_started ? (item.human_touched_sessions / item.sessions_started) * 100 : 0; + const tag = item.sessions_started ? 'button' : 'div'; + const attributes = item.sessions_started + ? `type="button" class="analytics-table-row ai-analytics-channel-grid ai-analytics-row-button" data-ai-channel="${escapeHtml(item.channel)}"` + : 'class="analytics-table-row ai-analytics-channel-grid"'; + return ` + <${tag} ${attributes}> + ${escapeHtml(analyticsChannelLabel(item.channel))} + ${formatAnalyticsNumber(item.sessions_started, 0)} + ${formatAnalyticsNumber(aiOnlyShare, 2)}% + ${formatAnalyticsNumber(humanTouchedShare, 2)}% + ${formatAnalyticsNumber(item.handoff_rate || 0, 2)}% + ${formatAiAnalyticsMetric('ai_latency_avg_ms', item.ai_latency_avg_ms)} + + `; + }) + .join('')} + `; +} + +function renderAiAnalyticsCoverage() { + const container = $('aiAnalyticsCoverage'); + const coverage = state.analytics.aiOverview?.coverage || emptyAiAnalyticsOverview().coverage; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!state.analytics.loading && Number(state.analytics.aiOverview?.totals?.sessions_started || 0) === 0) { + container.innerHTML = ''; + return; + } + container.innerHTML = ` +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_interaction_id || 0, 0)} +

            AI-сессий связаны с ID обращения

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_queue_id || 0, 0)} +

            AI-сессий имеют ID очереди через обращение или диалог

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_latency_turns || 0, 0)} +

            AI-сессий содержат данные по задержке ответов модели

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_terminal_state || 0, 0)} +

            AI-сессий дошли до финального состояния

            +
            +
            + ${formatAnalyticsNumber(coverage.sessions_with_handoff_reason || 0, 0)} +

            AI-сессий содержат причину передачи

            +
            +
            + `; +} + +function renderAiAnalyticsOutcomeTable() { + const container = $('aiAnalyticsOutcomeTable'); + const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_outcome) + ? state.analytics.aiOverview.breakdowns.by_outcome + : []; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!rows.length) { + container.innerHTML = '
            Срезы по исходам появятся, когда в окне будут AI-сессии.
            '; + return; + } + container.innerHTML = ` +
            + Исход + Сессии + Доля +
            + ${rows + .map((item) => { + const tag = item.sessions ? 'button' : 'div'; + const attributes = item.sessions + ? `type="button" class="analytics-table-row ai-analytics-outcome-grid ai-analytics-row-button" data-ai-outcome="${escapeHtml(item.outcome)}" data-ai-outcome-label="${escapeHtml(item.label || aiAnalyticsSliceLabel(item.outcome))}"` + : 'class="analytics-table-row ai-analytics-outcome-grid"'; + return ` + <${tag} ${attributes}> + ${escapeHtml(item.label || aiAnalyticsSliceLabel(item.outcome))} + ${formatAnalyticsNumber(item.sessions, 0)} + ${formatAnalyticsNumber(item.share || 0, 2)}% + + `; + }) + .join('')} + `; +} + +function renderAiAnalyticsReasonTable() { + const container = $('aiAnalyticsReasonTable'); + const rows = Array.isArray(state.analytics.aiOverview?.breakdowns?.by_handoff_reason) + ? state.analytics.aiOverview.breakdowns.by_handoff_reason + : []; + if (!container) { + return; + } + if (!aiAnalyticsSupportedChannel() || state.analytics.aiError) { + container.innerHTML = ''; + return; + } + if (!rows.length) { + container.innerHTML = '
            За выбранный период handoff reasons не найдены.
            '; + return; + } + container.innerHTML = ` +
            + Причина + Сессии + Доля +
            + ${rows + .map((item) => ` + + `) + .join('')} + `; +} + +const renderAnalyticsDashboardBase = renderAnalyticsDashboard; +const syncAnalyticsDrilldownControlsBase = syncAnalyticsDrilldownControls; +const analyticsDrilldownFiltersForCurrentSourceBase = analyticsDrilldownFiltersForCurrentSource; +const applyAnalyticsDrilldownLocalFiltersBase = applyAnalyticsDrilldownLocalFilters; +const resetAnalyticsDrilldownLocalFiltersBase = resetAnalyticsDrilldownLocalFilters; +const handleAnalyticsDrilldownSearchInputBase = handleAnalyticsDrilldownSearchInput; +const handleAnalyticsDrilldownSearchKeydownBase = handleAnalyticsDrilldownSearchKeydown; +const handleAnalyticsDrilldownStatusChangeBase = handleAnalyticsDrilldownStatusChange; +const handleAnalyticsDrilldownSortChangeBase = handleAnalyticsDrilldownSortChange; + +renderAnalyticsDashboard = function renderAnalyticsDashboard(rangeMeta = analyticsRangeFromControls()) { + renderAnalyticsDashboardBase(rangeMeta); + renderAiAnalyticsOutcomeTable(); + renderAiAnalyticsReasonTable(); +} + +analyticsDrilldownChips = function analyticsDrilldownChips() { + if (!state.drilldown.filters) { + return []; + } + const drilldownFilters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters); + const chips = []; + chips.push(state.drilldown.sourceLabel || 'Детализация'); + chips.push(`${formatTime(drilldownFilters.from_ts)} - ${formatTime(drilldownFilters.to_ts)}`); + if (drilldownFilters.channel) { + chips.push(analyticsChannelLabel(drilldownFilters.channel)); + } + if (drilldownFilters.queue_id) { + chips.push(analyticsQueueName(drilldownFilters.queue_id)); + } + if (drilldownFilters.agent_id) { + chips.push(`Агент: ${drilldownFilters.agent_id}`); + } + if (state.drilldown.mode === 'ai' && drilldownFilters.slice && drilldownFilters.slice !== 'all') { + chips.push(aiAnalyticsSliceLabel(drilldownFilters.slice)); + } + if (state.drilldown.mode === 'ai' && drilldownFilters.reason_key) { + chips.push(`Причина: ${drilldownFilters.reason_key}`); + } + if (drilldownFilters.status) { + chips.push(`Статус: ${state.drilldown.mode === 'ai' ? aiAnalyticsStatusLabel(drilldownFilters.status) : interactionStatusLabel(drilldownFilters.status)}`); + } + if (drilldownFilters.q) { + chips.push(`Поиск: ${drilldownFilters.q}`); + } + if (state.drilldown.mode === 'metric' && state.drilldown.metric) { + chips.push(analyticsMetricCoverageLabel(state.drilldown.metric)); + } else if (state.drilldown.mode === 'ai') { + chips.push(aiAnalyticsSortLabel(drilldownFilters)); + } else { + chips.push(analyticsDrilldownSortLabel(drilldownFilters)); + } + return chips; +} + +function analyticsDrilldownCurrentItemId(item) { + if (!item) { + return ''; + } + if (state.drilldown.mode === 'ai') { + return String(item.session_id || ''); + } + return String(item.interaction_id || ''); +} + +syncAnalyticsDrilldownControls = function syncAnalyticsDrilldownControls() { + if (state.drilldown.mode !== 'ai') { + syncAnalyticsDrilldownControlsBase(); + return; + } + const searchInput = $('analyticsDrilldownSearch'); + const statusSelect = $('analyticsDrilldownStatus'); + const sortSelect = $('analyticsDrilldownSort'); + const clearBtn = $('analyticsDrilldownClearBtn'); + const exportBtn = $('analyticsDrilldownExportBtn'); + const isOpen = state.drilldown.open; + const localFiltersEnabled = isOpen; + const filters = normalizeAnalyticsDrilldownFilters(state.drilldown.filters || {}); + + if (statusSelect) { + statusSelect.innerHTML = [ + '', + '', + '', + '', + '', + '', + ].join(''); + statusSelect.value = isOpen ? (filters.status || 'all') : 'all'; + statusSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (sortSelect) { + sortSelect.innerHTML = [ + '', + '', + '', + '', + '', + ].join(''); + sortSelect.value = analyticsDrilldownSortValue(filters); + sortSelect.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (searchInput) { + searchInput.value = isOpen ? filters.q : ''; + searchInput.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + searchInput.placeholder = 'ID сессии, диалога, обращения или причина'; + } + if (clearBtn) { + clearBtn.disabled = !localFiltersEnabled || state.drilldown.loading || state.drilldown.exporting; + } + if (exportBtn) { + exportBtn.disabled = !isOpen || state.drilldown.loading || state.drilldown.exporting || !state.drilldown.total; + exportBtn.textContent = state.drilldown.exporting ? 'Готовим CSV...' : 'Экспорт CSV'; + } +} + +analyticsDrilldownFiltersForCurrentSource = function analyticsDrilldownFiltersForCurrentSource() { + if (state.drilldown.mode !== 'ai') { + return analyticsDrilldownFiltersForCurrentSourceBase(); + } + const filters = analyticsDrilldownBaseFilters(); + filters.slice = 'all'; + filters.reason_key = ''; + filters.status = ''; + filters.q = ''; + filters.sort_by = 'created_at'; + filters.sort_dir = 'desc'; + if (state.drilldown.sourceType === 'ai-overview' && state.drilldown.sourceValue) { + filters.slice = state.drilldown.sourceValue; + } + if (state.drilldown.sourceType === 'ai-channel' && state.drilldown.sourceValue) { + filters.channel = state.drilldown.sourceValue; + } + if (state.drilldown.sourceType === 'ai-reason' && state.drilldown.sourceValue) { + filters.slice = 'handoff'; + filters.reason_key = state.drilldown.sourceValue; + } + return normalizeAnalyticsDrilldownFilters(filters); +} + +applyAnalyticsDrilldownLocalFilters = function applyAnalyticsDrilldownLocalFilters(nextFilters, options = {}) { + if (!state.drilldown.open || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return applyAnalyticsDrilldownLocalFiltersBase(nextFilters, options); + } + return; + } + state.drilldown.filters = normalizeAnalyticsDrilldownFilters(nextFilters); + state.drilldown.offset = 0; + state.drilldown.notice = ''; + void loadAnalyticsDrilldownPage({ preserveSelection: options.preserveSelection !== false }); +} + +resetAnalyticsDrilldownLocalFilters = function resetAnalyticsDrilldownLocalFilters() { + clearAnalyticsDrilldownSearchDebounce(); + if (!state.drilldown.open || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return resetAnalyticsDrilldownLocalFiltersBase(); + } + return; + } + state.drilldown.filters = analyticsDrilldownFiltersForCurrentSource(); + state.drilldown.offset = 0; + state.drilldown.notice = ''; + renderAnalyticsDrilldown(); + void loadAnalyticsDrilldownPage({ preserveSelection: true }); +} + +handleAnalyticsDrilldownSearchInput = function handleAnalyticsDrilldownSearchInput(event) { + if (!state.drilldown.open || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return handleAnalyticsDrilldownSearchInputBase(event); + } + return; + } + clearAnalyticsDrilldownSearchDebounce(); + const query = event.target?.value || ''; + analyticsDrilldownSearchDebounce = window.setTimeout(() => { + if (!state.drilldown.open || !state.drilldown.filters) { + return; + } + const normalizedQuery = query.trim(); + if ((state.drilldown.filters.q || '') === normalizedQuery) { + return; + } + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + q: normalizedQuery, + }); + }, 300); +} + +handleAnalyticsDrilldownSearchKeydown = function handleAnalyticsDrilldownSearchKeydown(event) { + if (event.key !== 'Enter' || !state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return handleAnalyticsDrilldownSearchKeydownBase(event); + } + return; + } + event.preventDefault(); + clearAnalyticsDrilldownSearchDebounce(); + const normalizedQuery = (event.target?.value || '').trim(); + if ((state.drilldown.filters.q || '') === normalizedQuery) { + return; + } + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + q: normalizedQuery, + }); +} + +handleAnalyticsDrilldownStatusChange = function handleAnalyticsDrilldownStatusChange(event) { + if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return handleAnalyticsDrilldownStatusChangeBase(event); + } + return; + } + const nextStatus = event.target?.value === 'all' ? '' : String(event.target?.value || ''); + if ((state.drilldown.filters.status || '') === nextStatus) { + return; + } + clearAnalyticsDrilldownSearchDebounce(); + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + status: nextStatus, + }); +} + +handleAnalyticsDrilldownSortChange = function handleAnalyticsDrilldownSortChange(event) { + if (!state.drilldown.open || !state.drilldown.filters || state.drilldown.mode === 'metric') { + if (state.drilldown.mode !== 'ai') { + return handleAnalyticsDrilldownSortChangeBase(event); + } + return; + } + const nextSort = parseAnalyticsDrilldownSort(event.target?.value || 'created_at:desc'); + if ( + (state.drilldown.filters.sort_by || 'created_at') === nextSort.sort_by + && (state.drilldown.filters.sort_dir || 'desc') === nextSort.sort_dir + ) { + return; + } + clearAnalyticsDrilldownSearchDebounce(); + applyAnalyticsDrilldownLocalFilters({ + ...state.drilldown.filters, + ...nextSort, + }); +} + +function analyticsTimeseriesIntervalForRange(rangeMeta) { + const durationMs = Math.max(0, rangeMeta.current.to.getTime() - rangeMeta.current.from.getTime()); + return durationMs <= 36 * 60 * 60 * 1000 ? 'hour' : 'day'; +} + +async function fetchAnalyticsTimeseries(range, metric, interval, overrides = {}) { + const params = new URLSearchParams(analyticsQuery(range, overrides)); + params.set('metric', metric || state.analytics.trendMetric || 'volume'); + params.set('interval', interval || 'day'); + return api('reporting', `reports/timeseries?${params.toString()}`); +} + +function trendPointEnvelope(metric, value) { + const envelope = emptyKpiEnvelope(); + if (metric === 'volume' || metric === 'total') { + envelope.volume.total = Number(value || 0); + return envelope; + } + if (metric === 'answered') { + envelope.volume.answered = Number(value || 0); + return envelope; + } + envelope.kpi[metric] = Number(value || 0); + return envelope; +} + +async function loadAnalyticsTrend(rangeMeta, requestId) { + const metric = state.analytics.trendMetric || 'volume'; + const interval = analyticsTimeseriesIntervalForRange(rangeMeta); + try { + const payload = await fetchAnalyticsTimeseries(rangeMeta.current, metric, interval); + if (requestId !== state.analytics.requestId) { + return []; + } + const resolvedInterval = payload?.interval || interval; + state.analytics.trendInterval = resolvedInterval; + const points = Array.isArray(payload?.points) ? payload.points : []; + return points.map((point) => ({ + ts: point.ts, + label: formatAnalyticsBucketLabel(point.ts, resolvedInterval), + payload: trendPointEnvelope(metric, point.value), + sampleSize: Number(point.sample_size || 0), + })); + } catch { + state.analytics.trendInterval = interval; + return []; + } +} + +function reportingExportQuery(range, extra = {}) { + const params = new URLSearchParams(analyticsQuery(range, extra)); + if (extra.metric) { + params.set('metric', extra.metric); + } + return params.toString(); +} + +async function exportAnalyticsCsv() { + const rangeMeta = state.analytics.lastRangeMeta || analyticsRangeFromControls(); + const dateStamp = new Date().toISOString().slice(0, 10); + try { + await downloadProxyFile( + 'reporting', + `reports/export?${reportingExportQuery(rangeMeta.current)}`, + `konturcc-analytics-${dateStamp}.csv`, + ); + state.analytics.statusMessage = `CSV выгружен в ${new Date().toLocaleTimeString('ru-RU', { + hour: '2-digit', + minute: '2-digit', + })}.`; + } catch (err) { + state.analytics.statusMessage = `Не удалось выгрузить CSV: ${err.message}`; + } + renderSavedAnalyticsViews(); +} + +async function loadSavedAnalyticsViews() { + try { + const items = await api('reporting', 'reports/views'); + state.analytics.savedViews = Array.isArray(items) + ? items + .filter((item) => item && typeof item === 'object') + .map((item) => normalizeSavedAnalyticsView(item)) + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + : []; + } catch (err) { + state.analytics.savedViews = []; + state.analytics.statusMessage = `Не удалось загрузить сохранённые виды: ${err.message}`; + } + if ( + state.analytics.activeViewId + && !state.analytics.savedViews.some((item) => item.id === state.analytics.activeViewId) + ) { + state.analytics.activeViewId = ''; + } + renderSavedAnalyticsViews(); +} + +async function saveAnalyticsView() { + syncAnalyticsStateFromControls(); + const input = $('analyticsSavedViewName'); + const name = input?.value.trim() || suggestAnalyticsViewName(); + try { + const view = await api('reporting', 'reports/views', { + method: 'POST', + body: JSON.stringify({ + id: state.analytics.activeViewId || null, + name, + snapshot: analyticsCurrentSnapshot(), + }), + }); + const normalized = normalizeSavedAnalyticsView(view); + const existingIndex = state.analytics.savedViews.findIndex((item) => item.id === normalized.id); + if (existingIndex >= 0) { + state.analytics.savedViews.splice(existingIndex, 1, normalized); + state.analytics.statusMessage = `Вид «${name}» обновлён.`; + } else { + state.analytics.savedViews.unshift(normalized); + state.analytics.statusMessage = `Вид «${name}» сохранён.`; + } + state.analytics.savedViews = state.analytics.savedViews + .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))) + .slice(0, 12); + state.analytics.activeViewId = normalized.id; + } catch (err) { + state.analytics.statusMessage = `Не удалось сохранить вид: ${err.message}`; + } + renderSavedAnalyticsViews(); +} + +async function deleteAnalyticsView() { + const activeView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null; + if (!activeView) { + state.analytics.statusMessage = 'Выберите сохранённый вид, чтобы удалить его.'; + renderSavedAnalyticsViews(); + return; + } + try { + await api('reporting', `reports/views/${encodeURIComponent(activeView.id)}`, { method: 'DELETE' }); + state.analytics.savedViews = state.analytics.savedViews.filter((item) => item.id !== activeView.id); + state.analytics.activeViewId = ''; + state.analytics.statusMessage = `Вид «${activeView.name}» удалён.`; + } catch (err) { + state.analytics.statusMessage = `Не удалось удалить вид: ${err.message}`; + } + renderSavedAnalyticsViews(); +} + +function wire() { + $('logoutBtn').addEventListener('click', logout); + $('refreshBtn').addEventListener('click', () => { + void loadAnalyticsDashboard(); + }); + $('analyticsRefreshBtn').addEventListener('click', () => { + void loadAnalyticsDashboard(); + }); + $('analyticsResetBtn').addEventListener('click', () => { + resetAnalyticsFilters(); + void loadAnalyticsDashboard(); + }); + $('analyticsPreset').addEventListener('change', handleAnalyticsPresetChange); + $('analyticsFrom').addEventListener('change', handleAnalyticsFilterChange); + $('analyticsTo').addEventListener('change', handleAnalyticsFilterChange); + $('analyticsQueue').addEventListener('change', handleAnalyticsFilterChange); + $('analyticsChannel').addEventListener('change', handleAnalyticsFilterChange); + $('analyticsCompareMode').addEventListener('change', handleAnalyticsFilterChange); + $('analyticsSavedViewSelect').addEventListener('change', applySelectedAnalyticsView); + $('analyticsSaveViewBtn').addEventListener('click', async () => { + await saveAnalyticsView(); + syncAnalyticsUrlState(); + }); + $('analyticsDeleteViewBtn').addEventListener('click', async () => { + await deleteAnalyticsView(); + syncAnalyticsUrlState(); + }); + $('analyticsExportBtn').addEventListener('click', exportAnalyticsCsv); + $('analyticsTrendMetric').addEventListener('change', () => { + syncAnalyticsStateFromControls(); + detachActiveAnalyticsView(); + renderSavedAnalyticsViews(); + renderAnalyticsTrendChart(); + syncAnalyticsUrlState(); + }); + $('voiceNameAnalyticsTrendMetric')?.addEventListener('change', () => { + syncAnalyticsStateFromControls(); + detachActiveAnalyticsView(); + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + void loadAnalyticsDashboard(); + }); + $('aiAnalyticsTrendMetric').addEventListener('change', () => { + syncAnalyticsStateFromControls(); + detachActiveAnalyticsView(); + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + void loadAnalyticsDashboard(); + }); + $('agentAnalyticsTrendMetric')?.addEventListener('change', () => { + syncAnalyticsStateFromControls(); + detachActiveAnalyticsView(); + renderSavedAnalyticsViews(); + syncAnalyticsUrlState(); + void loadAnalyticsDashboard(); + }); + $('analyticsOverview').addEventListener('click', handleAnalyticsOverviewClick); + $('analyticsChannelTable').addEventListener('click', handleAnalyticsChannelClick); + $('analyticsQueueTable').addEventListener('click', handleAnalyticsQueueClick); + $('agentAnalyticsTable')?.addEventListener('click', handleAgentAnalyticsClick); + $('analyticsDrilldownCloseBtn').addEventListener('click', closeAnalyticsDrilldown); + $('analyticsDrilldownBackdrop').addEventListener('click', closeAnalyticsDrilldown); + $('analyticsDrilldownList').addEventListener('click', handleAnalyticsDrilldownListClick); + $('analyticsDrilldownPrevBtn').addEventListener('click', handleAnalyticsDrilldownPrevPage); + $('analyticsDrilldownNextBtn').addEventListener('click', handleAnalyticsDrilldownNextPage); + $('analyticsDrilldownSearch').addEventListener('input', handleAnalyticsDrilldownSearchInput); + $('analyticsDrilldownSearch').addEventListener('keydown', handleAnalyticsDrilldownSearchKeydown); + $('analyticsDrilldownStatus').addEventListener('change', handleAnalyticsDrilldownStatusChange); + $('analyticsDrilldownSort').addEventListener('change', handleAnalyticsDrilldownSortChange); + $('analyticsDrilldownClearBtn').addEventListener('click', resetAnalyticsDrilldownLocalFilters); + $('analyticsDrilldownExportBtn').addEventListener('click', () => { + void exportAnalyticsDrilldownCsv(); + }); + window.addEventListener('keydown', handleAnalyticsDrilldownWindowKeydown); + window.addEventListener('message', handleOidcMessage); + $('aiAnalyticsOverview')?.addEventListener('click', handleAiAnalyticsOverviewClick); + $('aiAnalyticsChannelComparison')?.addEventListener('click', handleAiAnalyticsChannelClick); + $('aiAnalyticsOutcomeTable')?.addEventListener('click', handleAiAnalyticsOutcomeClick); + $('aiAnalyticsReasonTable')?.addEventListener('click', handleAiAnalyticsReasonClick); +} + +async function init() { + if (!restoreStoredSession()) { + window.location.href = '/'; + return; + } + if (!ensurePageAccess()) { + return; + } + wire(); + updateSessionInfo(); + const deepLinkState = parseAnalyticsDeepLinkState(); + analyticsUrlSyncSuspended = true; + try { + resetAnalyticsFilters(); + if (deepLinkState.hasSnapshot) { + applyAnalyticsSnapshot(deepLinkState.snapshot); + } + state.analytics.activeViewId = deepLinkState.activeViewId || ''; + syncAnalyticsControlsFromState(); + await Promise.all([ + checkGateway(), + loadOidcConfig(), + loadSavedAnalyticsViews(), + ]); + if (!deepLinkState.hasSnapshot && state.analytics.activeViewId) { + const savedView = state.analytics.savedViews.find((item) => item.id === state.analytics.activeViewId) || null; + if (savedView) { + applyAnalyticsSnapshot(savedView.snapshot); + syncAnalyticsControlsFromState(); + } + } else { + syncAnalyticsControlsFromState(); + } + await loadAnalyticsDashboard(true); + if (deepLinkState.drilldown) { + restoreAnalyticsDrilldownFromUrlState(deepLinkState.drilldown); + } + } finally { + analyticsUrlSyncSuspended = false; + } + syncAnalyticsUrlState(); +} + +init(); diff --git a/ui/analyst/index.html b/ui/analyst/index.html new file mode 100644 index 0000000..88c4ce7 --- /dev/null +++ b/ui/analyst/index.html @@ -0,0 +1,454 @@ + + + + + + KonturCC Аналитика + + + + + + + +
            + + +
            +
            +
            + Историческая аналитика сервиса +

            Периоды, каналы, очереди, AI и агентские срезы в одном окне.

            +
            +
            +
            Данные: проверка...
            + +
            +
            AN
            +
            +
            Центр аналитики
            +
            Историческая аналитика
            +
            +
            +
            +
            + +
            +
            +

            История сервиса

            +

            Аналитика контакт-центра

            +

            Исторические KPI, тренды и структура потока по каналам и очередям в одной витрине.

            +
            + +
            +
            +
            +

            Сводка периода

            +

            Ключевые метрики и тренды

            +

            Сравнивайте выбранный период с предыдущим окном и быстро переключайтесь между очередями и каналами.

            +
            +
            + + +
            +
            + +
            +
            + + +
            +
            + + +
            +
            + + +
            +
            + + +
            +
            + + +
            +
            + +
            +
            +
            + + +
            +

            Сохранённых видов пока нет.

            +
            +
            +
            + + +
            +
            + + +
            +
            + + + +
            +
            +
            + +

            Выберите период и при необходимости уточните канал или очередь.

            + + +
            + +
            + +
            +
            +
            +

            Сценарий имени

            +

            Сценарий сбора имени в голосе

            +

            Отдельная аналитика по тому, как сценарий берёт имя на старте, как помогает downstream AI и сколько звонков уходит оператору без подтверждённого имени.

            +
            +
            + + +
            +
            + +
            + +
            +
            +

            Тренд по voice name-flow

            +

            Динамика показывает, как меняется качество сбора имени по выбранному периоду, очереди и голосовому потоку.

            +
            +
            + +
            +

            Этапы сценария

            +

            Funnel помогает быстро увидеть, где чаще всего теряется имя: на старте, в follow-up или уже перед handoff.

            +
            +
            +
            + +
            +
            +

            По языкам

            +

            Сравнение сценария по языкам помогает увидеть, где стартовый сбор имени работает стабильнее, а где чаще требуется downstream AI.

            +
            +
            + +
            +

            По очередям

            +

            Срез по очередям показывает, в каких очередях voice name-flow чаще доходит до handoff без подтверждённого имени.

            +
            +
            +
            + +
            +

            Handoff по имени

            +

            Здесь видно, сколько звонков оператор получил уже с подтверждённым именем, а сколько ушло без него.

            +
            +
            +
            + +
            +
            +
            +

            AI-аналитика

            +

            AI-сессии в цифровых каналах

            +

            Отдельный блок по Telegram и WhatsApp: закрытие AI, передача оператору, задержка ответа и закрытия без оператора без доступа к текстам сообщений.

            +
            +
            + + +
            +
            + +
            + +
            +
            +

            AI-тренд по периоду

            +

            Динамика AI по выбранной метрике строится по тем же фильтрам периода, канала и очереди, что и основная витрина.

            +
            +
            + +
            +

            Покрытие AI-данных

            +

            Проверяем, насколько AI-сессии связаны с обращениями, очередями и данными по задержке ответа для корректной интерпретации отчёта.

            +
            +
            +
            + +
            +

            AI и оператор по каналам

            +

            Сравнение полностью автоматических и затронутых оператором сессий по Telegram и WhatsApp без текстов сообщений, записей и AI-сводок.

            +
            +
            + +
            +
            +

            Исходы AI-сессий

            +

            Клик по исходу открывает полноэкранную детализацию по AI-сессиям только на уровне метаданных.

            +
            +
            + +
            +

            Причины передачи оператору

            +

            Нажмите на причину, чтобы открыть срез по передачам оператору с той же группой причин.

            +
            +
            +
            +
            + +
            +
            +
            +

            Агенты

            +

            Агентская аналитика

            +

            Нагрузка, обработка и текущие состояния агентов по фактам reporting и данным supervisor без локальных заглушек.

            +
            +
            + +
            +
            + +
            +
            +
            +
            +

            Тренд по агентам

            +

            Динамика по агентам помогает оценить нагрузку, стабильность обработки и изменение FCR по выбранному окну.

            +
            +
            + + +
            +
            +
            +
            + +
            +

            Команды и очереди

            +

            Сводка по командам и очередям, где сейчас сосредоточена агентская нагрузка.

            +
            +
            +
            + +
            +

            Агенты по выбранному периоду

            +

            Клик по строке открывает полноэкранную детализацию по обращениям выбранного агента без изменения фильтров витрины.

            +
            +
            + +
            +
            +

            Смены

            +

            Сравнение ночной, дневной и вечерней смен по обращениям, обработке и FCR.

            +
            +
            +
            +
            + +
            +
            +
            +
            +

            Тренд по периоду

            +

            Динамика по выбранной метрике помогает быстро увидеть изменение нагрузки и качества.

            +
            +
            + + +
            +
            +
            +
            + +
            +

            Каналы

            +

            Сравнение каналов по объёму, обработке и доле ответа за выбранный период.

            +

            Нажмите на строку, чтобы открыть точную детализацию по обращениям этого канала.

            +
            +
            +
            + +
            +
            +

            Очереди

            +

            Очереди отсортированы по объёму. Нажмите на строку, чтобы сфокусировать всю витрину на одной очереди.

            +

            Клик по строке открывает точный срез по очереди и не меняет фильтры витрины.

            +
            +
            +
            +
            +
            +
            +
            + + + + + + + + + + diff --git a/ui/analyst/mock-analytics.json b/ui/analyst/mock-analytics.json new file mode 100644 index 0000000..04c2422 --- /dev/null +++ b/ui/analyst/mock-analytics.json @@ -0,0 +1,207 @@ +{ + "queue_options": [ + { "queue_id": "sales_kz", "name": "Продажи KZ" }, + { "queue_id": "support_kz", "name": "Поддержка KZ" }, + { "queue_id": "vip_kz", "name": "VIP-линия" }, + { "queue_id": "retention_kz", "name": "Удержание" } + ], + "agents": [ + "agent.aigerim", + "agent.dias", + "agent.nurdaulet", + "agent.madina", + "agent.ruslan", + "agent.tomiris" + ], + "queue_factors": { + "sales_kz": 1.08, + "support_kz": 0.96, + "vip_kz": 0.58, + "retention_kz": 0.72 + }, + "channel_base": { + "voice": { "total": 620, "answered": 584, "abandoned": 36 }, + "telegram": { "total": 214, "answered": 205, "abandoned": 9 }, + "whatsapp": { "total": 182, "answered": 171, "abandoned": 11 }, + "webchat": { "total": 141, "answered": 132, "abandoned": 9 }, + "email": { "total": 108, "answered": 94, "abandoned": 14 } + }, + "ai_reason_labels": { + "requested_human": "Запрос оператора", + "knowledge_or_tool_gap": "Не хватило знаний или инструмента", + "policy_or_sensitive": "Чувствительный сценарий", + "delivery_or_runtime_error": "Ошибка доставки или runtime", + "manual_claim": "Ручной takeover", + "other": "Другая причина" + }, + "interaction_statuses": [ + "closed", + "in_progress", + "closed", + "new", + "closed", + "escalated" + ], + "agent_state_cycle": [ + "READY", + "BUSY", + "READY", + "BREAK", + "BUSY", + "OFFLINE" + ], + "coverage_note": "Показаны демонстрационные данные для временного наполнения витрины.", + "dashboard_coverage": { + "implemented_metrics": ["SL", "ASA", "AHT", "Abandon", "FCR", "DigitalShare"], + "dimensions": ["queue", "channel", "agent", "shift"], + "supported_filters": ["preset", "from_ts", "to_ts", "queue_id", "channel"] + }, + "ai_channel_rows": [ + { + "channel": "telegram", + "sessions_started": 176, + "ai_only_sessions": 106, + "human_touched_sessions": 58, + "handoff_rate": 24.4, + "ai_latency_avg_ms": 1380, + "containment_rate": 60.2, + "closed_without_operator_rate": 52.1 + }, + { + "channel": "whatsapp", + "sessions_started": 142, + "ai_only_sessions": 80, + "human_touched_sessions": 54, + "handoff_rate": 28.9, + "ai_latency_avg_ms": 1524, + "containment_rate": 56.4, + "closed_without_operator_rate": 48.6 + } + ], + "voice_name_flow": { + "totals": { + "scenario_calls": 196, + "start_obtained": 118, + "downstream_ai_obtained": 34, + "followup_required": 18, + "name_not_obtained": 26, + "manual_corrected": 12, + "handoff_confirmed_name": 14, + "handoff_unconfirmed_name": 28, + "needed_downstream": 78 + }, + "languages": [ + { + "language": "ru", + "share": 0.58, + "start_capture_rate": 66.2, + "downstream_rescue_rate": 47.4, + "handoff_unconfirmed_rate": 31.8, + "manual_correction_rate": 14.6 + }, + { + "language": "kz", + "share": 0.29, + "start_capture_rate": 61.7, + "downstream_rescue_rate": 51.3, + "handoff_unconfirmed_rate": 28.4, + "manual_correction_rate": 11.8 + }, + { + "language": "unknown", + "share": 0.13, + "start_capture_rate": 44.8, + "downstream_rescue_rate": 36.1, + "handoff_unconfirmed_rate": 46.5, + "manual_correction_rate": 23.0 + } + ], + "queues": [ + { + "queue_id": "sales_kz", + "share": 0.31, + "start_capture_rate": 68.4, + "downstream_rescue_rate": 49.6, + "handoff_unconfirmed_rate": 24.1, + "manual_correction_rate": 11.2 + }, + { + "queue_id": "support_kz", + "share": 0.39, + "start_capture_rate": 61.2, + "downstream_rescue_rate": 46.9, + "handoff_unconfirmed_rate": 31.8, + "manual_correction_rate": 15.4 + }, + { + "queue_id": "vip_kz", + "share": 0.11, + "start_capture_rate": 73.5, + "downstream_rescue_rate": 52.8, + "handoff_unconfirmed_rate": 18.6, + "manual_correction_rate": 8.9 + }, + { + "queue_id": "retention_kz", + "share": 0.19, + "start_capture_rate": 55.1, + "downstream_rescue_rate": 39.4, + "handoff_unconfirmed_rate": 37.9, + "manual_correction_rate": 19.7 + } + ], + "coverage": { + "sessions_with_start_decision": 192, + "sessions_with_final_ai_state": 184, + "sessions_with_manual_overlay": 12, + "note": "Мок-данные по voice name-flow показывают стартовый захват имени, добор AI и ручное исправление оператором." + }, + "trend_profiles": { + "scenario_calls": { "base": 24, "step": 2.4, "cycle": 4 }, + "start_capture_rate": { "base": 60.5, "step": 2.2, "cycle": 4 }, + "downstream_rescue_rate": { "base": 40.2, "step": 1.9, "cycle": 5 }, + "handoff_unconfirmed_rate": { "base": 33.1, "step": 2.4, "cycle": 4 }, + "manual_correction_rate": { "base": 14.4, "step": 1.5, "cycle": 3 } + }, + "empty_note": "За выбранный период нет звонков, прошедших через сценарий сбора имени." + }, + "voice_name_labels": { + "scenario_calls": "Звонки в сценарии", + "start_obtained": "Имя взято сразу", + "needed_downstream": "Потребовался AI после старта", + "downstream_ai_obtained": "Имя добрал AI после follow-up", + "handoff_confirmed_name": "Передача с подтверждённым именем", + "handoff_unconfirmed_name": "Передача без подтверждённого имени", + "confirmed_name": "Передача с подтверждённым именем", + "unconfirmed_name": "Передача без подтверждённого имени" + }, + "agent_shift_rows": [ + { + "shift_key": "night", + "label": "Ночная смена", + "agents_with_activity": 2, + "interactions_total": 18, + "answered_total": 17, + "avg_handle_seconds": 356, + "fcr_rate": 69.5 + }, + { + "shift_key": "day", + "label": "Дневная смена", + "agents_with_activity": 5, + "interactions_total": 52, + "answered_total": 53, + "avg_handle_seconds": 318, + "fcr_rate": 76.8 + }, + { + "shift_key": "evening", + "label": "Вечерняя смена", + "agents_with_activity": 3, + "interactions_total": 30, + "answered_total": 30, + "avg_handle_seconds": 334, + "fcr_rate": 73.4 + } + ] +} diff --git a/ui/login/app.js b/ui/login/app.js new file mode 100644 index 0000000..33133b3 --- /dev/null +++ b/ui/login/app.js @@ -0,0 +1,193 @@ +const SESSION_STORAGE_KEY = 'cc_session'; + +const state = { + user: 'admin', + role: 'admin', + token: null, + authSource: 'local', + oidc: { + enabled: false, + loginPath: '/auth/oidc/start?return_mode=popup', + providerLabel: 'Keycloak', + }, +}; + +const $ = (id) => document.getElementById(id); + +function destinationForRole(role) { + if (role === 'admin') { + return '/admin'; + } + if (role === 'supervisor') { + return '/supervisor'; + } + if (role === 'analyst') { + return '/analyst'; + } + return '/operator'; +} + +function syncSessionFromInputs() { + state.user = $('sessionUser').value.trim() || 'admin'; +} + +function writeLog(message, payload) { + const suffix = payload ? ` ${JSON.stringify(payload, null, 2)}` : ''; + $('loginLog').textContent = `${message}${suffix}`; +} + +function persistSession(data) { + const payload = { + access_token: data.access_token, + user: data.username || state.user, + role: data.role, + auth_source: data.auth_source || 'local', + provider: data.provider || null, + full_name: data.full_name || null, + }; + window.localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(payload)); +} + +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'; + $('sessionUser').value = state.user; + updateSessionInfo(data.provider ? `provider: ${data.provider}` : ''); + persistSession(data); +} + +async function api(service, path, options = {}) { + syncSessionFromInputs(); + const headers = { + 'Content-Type': 'application/json', + ...(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: 'Non-JSON response' }; + } + if (!response.ok) { + throw new Error(`${response.status}: ${JSON.stringify(data)}`); + } + return data; +} + +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) { + writeLog('Корпоративный вход недоступен.'); + return; + } + const popup = window.open(`/proxy/auth${state.oidc.loginPath}`, 'oidc-login', 'width=620,height=760'); + if (!popup) { + writeLog('Не удалось открыть окно корпоративного входа.'); + return; + } + writeLog('Открыт корпоративный вход.', { provider: 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); + writeLog('Корпоративный вход выполнен.', { user: state.user, role: state.role }); + window.location.href = destinationForRole(state.role); + return; + } + if (event.data.type === 'oidc-error') { + writeLog('Корпоративный вход не выполнен.', { error: event.data.message || 'unknown' }); + } +} + +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', + }); + writeLog('Локальный вход выполнен.', { user: state.user, role: data.role }); + window.location.href = destinationForRole(data.role); + } catch (err) { + $('sessionInfo').textContent = `Ошибка входа: ${err.message}`; + writeLog('Вход не выполнен.', { error: err.message }); + } +} + +function wire() { + $('loginBtn').addEventListener('click', login); + $('corporateLoginBtn').addEventListener('click', startCorporateLogin); + $('refreshBtn').addEventListener('click', loadOidcConfig); + window.addEventListener('message', handleOidcMessage); +} + +async function init() { + wire(); + updateSessionInfo(); + await loadOidcConfig(); + const raw = window.localStorage.getItem(SESSION_STORAGE_KEY); + if (raw) { + try { + const payload = JSON.parse(raw); + if (payload?.access_token) { + state.token = payload.access_token; + state.user = payload.user || state.user; + state.role = payload.role || state.role; + state.authSource = payload.auth_source || 'local'; + $('sessionUser').value = state.user; + updateSessionInfo(payload.provider ? `provider: ${payload.provider}` : ''); + writeLog('Найдена активная сессия.', { user: state.user, role: state.role }); + } + } catch { + window.localStorage.removeItem(SESSION_STORAGE_KEY); + } + } +} + +init(); diff --git a/ui/login/index.html b/ui/login/index.html new file mode 100644 index 0000000..7491f7e --- /dev/null +++ b/ui/login/index.html @@ -0,0 +1,62 @@ + + + + + + KonturCC Login + + + + + + + +
            +
            + + +
            +

            Session

            +

            Авторизация

            +

            После входа система автоматически переведёт вас в нужный рабочий экран по роли.

            +
            + + + +

            Токен: отсутствует

            +
            Ожидание входа.
            +
            +
            + + + + diff --git a/ui/operator/access-guards.js b/ui/operator/access-guards.js new file mode 100644 index 0000000..5e3a9c6 --- /dev/null +++ b/ui/operator/access-guards.js @@ -0,0 +1,70 @@ +(() => { + const SESSION_STORAGE_KEY = 'cc_session'; + const SUPERVISOR_REALTIME_PATH = '/proxy/supervisor/supervisor/realtime'; + + function currentRole() { + try { + const raw = window.localStorage.getItem(SESSION_STORAGE_KEY); + if (!raw) { + return ''; + } + const payload = JSON.parse(raw); + return String(payload?.role || '').toLowerCase(); + } catch { + return ''; + } + } + + function roleFromRequest(input, init) { + try { + const headers = new Headers((init && init.headers) || (input && input.headers) || undefined); + return String(headers.get('X-Role') || '').toLowerCase(); + } catch { + return ''; + } + } + + function urlFromRequest(input) { + if (typeof input === 'string') { + return input; + } + if (input && typeof input.url === 'string') { + return input.url; + } + return ''; + } + + function isRestrictedSupervisorRealtime(input, init) { + const url = urlFromRequest(input); + if (!url.includes(SUPERVISOR_REALTIME_PATH)) { + return false; + } + const role = roleFromRequest(input, init) || currentRole(); + return role === 'operator'; + } + + function restrictedSupervisorResponse() { + return new Response( + JSON.stringify({ + agents: { total: 0, by_state: {}, items: [] }, + queues: [], + timestamp: new Date().toISOString(), + restricted: true, + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } + + const originalFetch = window.fetch.bind(window); + window.fetch = function patchedFetch(input, init) { + if (isRestrictedSupervisorRealtime(input, init)) { + return Promise.resolve(restrictedSupervisorResponse()); + } + return originalFetch(input, init); + }; +})(); diff --git a/ui/operator/app.js b/ui/operator/app.js new file mode 100644 index 0000000..a275fb6 --- /dev/null +++ b/ui/operator/app.js @@ -0,0 +1,7456 @@ +const state = { + user: 'admin', + role: 'admin', + token: null, + authSource: 'local', + fullName: null, + features: { + whatsapp: false, + }, + logLines: [], + interactions: [], + oidc: { + enabled: false, + loginPath: '/auth/oidc/start?return_mode=popup', + providerLabel: 'Keycloak', + }, + liveCalls: { + items: [], + recentItems: [], + selectedCallId: '', + pollTimer: null, + pendingAction: '', + aiSummaries: {}, + aiSummaryPending: {}, + nameEditor: { + open: false, + mode: 'panel', + callId: '', + customerId: '', + draft: '', + saving: false, + error: '', + }, + }, + customers: { + items: [], + page: 1, + pageSize: 7, + selectedCustomerId: '', + leadFormOpen: false, + historyById: {}, + pendingHistoryById: {}, + historyErrorsById: {}, + nameEditor: { + open: false, + customerId: '', + draft: '', + saving: false, + error: '', + flash: '', + }, + }, + telegram: { + threads: [], + selectedThreadId: '', + messages: [], + selectedThreadSummary: null, + searchQuery: '', + messageSearchQuery: '', + messageSearchOpen: false, + operatorTrayOpen: false, + pendingAction: '', + pollTimer: null, + }, + messenger: { + conversations: [], + selectedConversationId: '', + messages: [], + selectedSummary: null, + activeFilter: 'all', + composerText: '', + backendError: '', + pendingAction: '', + pollTimer: null, + }, + whatsapp: { + chats: [], + selectedChatId: '', + searchQuery: '', + activeFilter: 'all', + composerText: '', + selectedThreadSummary: null, + pendingAction: '', + pollTimer: null, + mode: 'idle', + backendError: '', + mockFallbackLogged: false, + }, + browserPhone: { + config: null, + ua: null, + registerer: null, + session: null, + status: 'Не настроен', + connected: false, + connecting: false, + incoming: false, + muted: false, + micDeviceId: '', + speakerDeviceId: '', + autoClaimCallId: '', + popupCallId: '', + endingCallId: '', + callPhase: '', + autoClaimInFlight: false, + warning: '', + settingsOpen: false, + sessionStartedAt: '', + localStream: null, + localStreamPromise: null, + ringtoneContext: null, + ringtoneTimer: null, + ringtoneActive: false, + }, +}; + +const $ = (id) => document.getElementById(id); +const SESSION_STORAGE_KEY = 'cc_session'; +const DEMO_ASSIGNEE = 'operator_a'; +const DEMO_QUEUE = 'line2'; +const ROLE_LABELS = { + admin: 'Администратор', + supervisor: 'Супервизор', + operator: 'Оператор', + analyst: 'Аналитик', +}; + +const STATUS_META = { + new: { label: 'новое', className: 'status-new' }, + in_progress: { label: 'назначено', className: 'status-in-progress' }, + escalated: { label: 'эскалировано', className: 'status-escalated' }, + closed: { label: 'закрыто', className: 'status-closed' }, + abandoned: { label: 'потеряно', className: 'status-closed' }, +}; + +const CHANNEL_LABELS = { + voice: 'Голос', + telegram: 'Telegram', + whatsapp: 'WhatsApp', + webchat: 'Webchat', + email: 'Email', +}; + +const TELEGRAM_AVATAR_PALETTES = [ + ['#5bc7ff', '#288cff'], + ['#7a8cff', '#5664f5'], + ['#52d6b5', '#0f9f8a'], + ['#ffa96e', '#f57b3d'], + ['#ff88b6', '#f34b86'], + ['#8a7dff', '#5d58ea'], +]; + +const WHATSAPP_AVATAR_PALETTES = [ + ['#2dd4bf', '#0f766e'], + ['#60a5fa', '#1d4ed8'], + ['#f59e0b', '#c2410c'], + ['#a78bfa', '#6d28d9'], + ['#f472b6', '#be185d'], + ['#34d399', '#047857'], + ['#fb7185', '#be123c'], +]; + +const WHATSAPP_MOCK_CHATS = [ + { + id: 'wa-amy-chen', + title: 'Amy Chen', + handle: '@amyc', + statusLine: 'в сети', + isGroup: false, + unreadCount: 0, + pinned: true, + muted: false, + lastMessageAt: '2026-03-10T18:43:00+05:00', + lastMessagePreview: 'Отлично, пришлите образцы и счёт.', + messages: [ + { + id: 'wa-amy-1', + direction: 'in', + text: 'Здравствуйте! В обновлённое предложение вошли матовые бутылки?', + createdAt: '2026-03-10T18:15:00+05:00', + }, + { + id: 'wa-amy-2', + direction: 'out', + text: 'Да, я добавил и матовое покрытие, и вариант с индивидуальной этикеткой.', + createdAt: '2026-03-10T18:18:00+05:00', + deliveryStatus: 'read', + }, + { + id: 'wa-amy-3', + direction: 'in', + text: 'Отлично, пришлите образцы и счёт.', + createdAt: '2026-03-10T18:43:00+05:00', + }, + ], + }, + { + id: 'wa-design-ops', + title: 'Design Ops', + handle: '6 participants', + statusLine: 'Анна, Тимур, Лейла и ещё 3 участника', + isGroup: true, + unreadCount: 12, + pinned: true, + muted: true, + lastMessageAt: '2026-03-10T17:58:00+05:00', + lastMessagePreview: 'Лейла: hero-карточки согласованы, остались только иконки.', + messages: [ + { + id: 'wa-design-1', + direction: 'system', + text: 'Today', + createdAt: '2026-03-10T09:00:00+05:00', + }, + { + id: 'wa-design-2', + direction: 'in', + author: 'Timur', + text: 'Залил обновлённые отступы сайдбара и тёмные токены.', + createdAt: '2026-03-10T17:28:00+05:00', + }, + { + id: 'wa-design-3', + direction: 'out', + text: 'Отлично. Оставьте rail компактным, а действия с чатом перенесите в header.', + createdAt: '2026-03-10T17:35:00+05:00', + deliveryStatus: 'read', + }, + { + id: 'wa-design-4', + direction: 'in', + author: 'Leila', + text: 'Hero-карточки согласованы, остались только иконки.', + createdAt: '2026-03-10T17:58:00+05:00', + }, + ], + }, + { + id: 'wa-marat-logistics', + title: 'Marat Logistics', + handle: '+7 707 555 2001', + statusLine: 'был в сети сегодня в 16:42', + isGroup: false, + unreadCount: 0, + pinned: false, + muted: false, + lastMessageAt: '2026-03-10T16:41:00+05:00', + lastMessagePreview: 'Грузовик будет у ворот склада через 20 минут.', + messages: [ + { + id: 'wa-marat-1', + direction: 'in', + text: 'Грузовик будет у ворот склада через 20 минут.', + createdAt: '2026-03-10T16:41:00+05:00', + }, + { + id: 'wa-marat-2', + direction: 'out', + text: 'Принято. На пост охраны уже передали обновлённую накладную.', + createdAt: '2026-03-10T16:44:00+05:00', + deliveryStatus: 'delivered', + }, + ], + }, + { + id: 'wa-leila', + title: 'Leila', + handle: '@leila.studio', + statusLine: 'печатает...', + isGroup: false, + unreadCount: 1, + pinned: false, + muted: false, + lastMessageAt: '2026-03-10T15:26:00+05:00', + lastMessagePreview: 'Можно перенести ревью на 18:30?', + messages: [ + { + id: 'wa-leila-1', + direction: 'out', + text: 'Могу быстро посмотреть в 18:00, если так удобнее.', + createdAt: '2026-03-10T15:20:00+05:00', + deliveryStatus: 'read', + }, + { + id: 'wa-leila-2', + direction: 'in', + text: 'Можно перенести ревью на 18:30?', + createdAt: '2026-03-10T15:26:00+05:00', + }, + ], + }, + { + id: 'wa-family-trip', + title: 'Family Trip 2026', + handle: '8 participants', + statusLine: 'Закреплённая группа', + isGroup: true, + unreadCount: 0, + pinned: false, + muted: false, + lastMessageAt: '2026-03-10T13:08:00+05:00', + lastMessagePreview: 'Арман: я забронировал отель на пятницу.', + messages: [ + { + id: 'wa-family-1', + direction: 'in', + author: 'Arman', + text: 'Я забронировал отель на пятницу.', + createdAt: '2026-03-10T13:08:00+05:00', + }, + { + id: 'wa-family-2', + direction: 'out', + text: 'Отлично. Позже отправлю сюда маршрут и детали заселения.', + createdAt: '2026-03-10T13:11:00+05:00', + deliveryStatus: 'read', + }, + ], + }, + { + id: 'wa-studio-intake', + title: 'Studio Intake', + handle: '+7 701 222 8811', + statusLine: 'был в сети вчера в 22:10', + isGroup: false, + unreadCount: 4, + pinned: false, + muted: true, + lastMessageAt: '2026-03-09T22:08:00+05:00', + lastMessagePreview: 'Можете ещё раз прислать референсы по упаковке?', + messages: [ + { + id: 'wa-studio-1', + direction: 'in', + text: 'Можете ещё раз прислать референсы по упаковке?', + createdAt: '2026-03-09T22:08:00+05:00', + }, + { + id: 'wa-studio-2', + direction: 'out', + text: 'Да, сейчас отправлю свежий PDF и подборку образцов.', + createdAt: '2026-03-09T22:12:00+05:00', + deliveryStatus: 'read', + }, + ], + }, + { + id: 'wa-oleg-voice', + title: 'Oleg Voice', + handle: '+7 705 123 4490', + statusLine: 'был в сети вчера в 20:31', + isGroup: false, + unreadCount: 0, + pinned: false, + muted: false, + lastMessageAt: '2026-03-09T19:18:00+05:00', + lastMessagePreview: 'Спасибо, заметки для handoff хватило, чтобы продолжить.', + messages: [ + { + id: 'wa-oleg-1', + direction: 'out', + text: 'Оставил handoff-сводку в карточке задачи и отправил заметки по звонку.', + createdAt: '2026-03-09T19:08:00+05:00', + deliveryStatus: 'read', + }, + { + id: 'wa-oleg-2', + direction: 'in', + text: 'Спасибо, заметки для handoff хватило, чтобы продолжить.', + createdAt: '2026-03-09T19:18:00+05:00', + }, + ], + }, +]; + +const BOARD_COLUMNS = [ + { key: 'new', title: 'Новые' }, + { key: 'in_progress', title: 'В работе' }, + { key: 'escalated', title: '2 линия' }, + { key: 'closed', title: 'Закрытые' }, +]; + +const UNIFIED_INBOX_COLUMNS = [ + { key: 'new', title: 'Новые', emptyMessage: 'Новых задач сейчас нет.' }, + { key: 'mine', title: 'Мои', emptyMessage: 'За вами пока ничего не закреплено.' }, + { key: 'ai', title: 'AI handoff', emptyMessage: 'Передач от AI сейчас нет.' }, + { key: 'escalated', title: 'Эскалации', emptyMessage: 'Эскалаций сейчас нет.' }, + { key: 'calls', title: 'Активные звонки', emptyMessage: 'Активных звонков сейчас нет.' }, +]; + +const MESSENGER_FILTERS = { + all: 'Все', + new: 'Новые', + mine: 'Мои', + ai: 'AI handoff', +}; + +const LIVE_TELEPHONY_LABELS = { + ringing: 'Звонит', + claimed: 'Взято', + connected: 'Соединено', + ended: 'Завершено', + failed: 'Ошибка', +}; + +const VOICE_AI_STATE_META = { + greeting: { label: 'AI активен', className: 'ai-active' }, + listening: { label: 'AI активен', className: 'ai-active' }, + thinking: { label: 'AI думает', className: 'ai-thinking' }, + speaking: { label: 'AI активен', className: 'ai-active' }, + active: { label: 'AI активен', className: 'ai-active' }, + handoff_requested: { label: 'Ждёт человека', className: 'ai-handoff' }, + handoff_required: { label: 'Ждёт человека', className: 'ai-handoff' }, + human_owned: { label: 'У оператора', className: 'ai-human' }, + error: { label: 'Ошибка AI', className: 'ai-error' }, + closed: { label: 'AI завершён', className: 'ai-muted' }, +}; + +const OPERATOR_VIEW_IDS = { + workspace: 'workspaceView', + messages: 'messagesView', + customers: 'customersView', + 'customer-profile': 'customerProfileView', + telegram: 'telegramView', + whatsapp: 'whatsappView', + calls: 'callsView', +}; + +function featureEnabled(feature) { + return Boolean(state.features?.[feature]); +} + +function isOperatorViewEnabled(view) { + if (view === 'whatsapp') { + return featureEnabled('whatsapp'); + } + return true; +} + +function stopWhatsappPolling() { + if (state.whatsapp.pollTimer) { + window.clearInterval(state.whatsapp.pollTimer); + state.whatsapp.pollTimer = null; + } +} + +function resetWhatsappState(mode = 'idle') { + state.whatsapp.chats = []; + state.whatsapp.selectedChatId = ''; + state.whatsapp.searchQuery = ''; + state.whatsapp.activeFilter = 'all'; + state.whatsapp.composerText = ''; + state.whatsapp.selectedThreadSummary = null; + state.whatsapp.pendingAction = ''; + state.whatsapp.backendError = ''; + state.whatsapp.mockFallbackLogged = false; + state.whatsapp.mode = mode; +} + +function syncWhatsappFeatureVisibility() { + const enabled = featureEnabled('whatsapp'); + document.querySelectorAll('[data-feature="whatsapp"]').forEach((element) => { + element.hidden = !enabled; + }); + document.querySelectorAll('[data-feature-option="whatsapp"]').forEach((element) => { + element.hidden = !enabled; + element.disabled = !enabled; + }); + if (!enabled) { + stopWhatsappPolling(); + resetWhatsappState('disabled'); + if ($('interactionChannel')?.value === 'whatsapp') { + $('interactionChannel').value = 'voice'; + } + return; + } + if (state.whatsapp.mode === 'disabled') { + resetWhatsappState('idle'); + } +} + +function syncSessionFromInputs() { + state.user = $('sessionUser').value.trim() || 'admin'; + state.role = $('sessionRole').value || 'admin'; +} + +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-roles]').forEach((link) => { + const allowed = (link.dataset.roles || '') + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + link.style.display = !allowed.length || allowed.includes(state.role) ? '' : 'none'; + }); + document.querySelectorAll('.nav-group').forEach((group) => { + const shellLinks = [...group.querySelectorAll('[data-shell]')]; + if (!shellLinks.length) { + group.style.display = ''; + return; + } + const hasVisibleShellLink = shellLinks.some((link) => link.style.display !== 'none'); + group.style.display = hasVisibleShellLink ? '' : 'none'; + }); +} + +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() { + if (state.liveCalls.pollTimer) { + window.clearInterval(state.liveCalls.pollTimer); + state.liveCalls.pollTimer = null; + } + if (state.telegram.pollTimer) { + window.clearInterval(state.telegram.pollTimer); + state.telegram.pollTimer = null; + } + stopMessengerPolling(); + stopWhatsappPolling(); + clearStoredSession(); + window.location.href = '/'; +} + +function shortJson(payload) { + if (!payload || typeof payload !== 'object') { + return ''; + } + const parts = Object.entries(payload) + .filter(([, value]) => value !== null && value !== undefined && value !== '') + .slice(0, 3) + .map(([key, value]) => { + const printable = typeof value === 'object' ? JSON.stringify(value) : value; + return `${key}=${printable}`; + }); + return parts.join(', '); +} + +function log(message, payload) { + const ts = new Date().toLocaleTimeString(); + const suffix = shortJson(payload); + const line = suffix ? `[${ts}] ${message} (${suffix})` : `[${ts}] ${message}`; + state.logLines.unshift(line); + state.logLines = state.logLines.slice(0, 14); + const target = $('eventLog'); + if (target) { + target.textContent = state.logLines.join('\n'); + } +} + +function setOutput(targetId, payload) { + $(targetId).textContent = JSON.stringify(payload, null, 2); +} + +function setEmptyList(targetId, message) { + $(targetId).innerHTML = `
          • ${message}
          • `; +} + +function setEmptyBlock(targetId, message) { + $(targetId).innerHTML = `
            ${message}
            `; +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function escapeRegExp(value) { + return String(value ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function customerHashSeed(item, fallback = 0) { + const raw = `${item?.customer_id || ''}|${item?.display_name || ''}|${(item?.phones || []).join(',') || ''}`; + return [...raw].reduce((acc, char, index) => (acc + char.charCodeAt(0) * (index + 1)) % 997, fallback); +} + +function normalizedCustomerTags(item) { + return Array.isArray(item?.tags) + ? item.tags.map((tag) => String(tag || '').trim().toLowerCase()).filter(Boolean) + : []; +} + +function isFacebookCustomer(item) { + const tags = normalizedCustomerTags(item); + return tags.includes('facebook') || tags.includes('facebook-lead'); +} + +function isFacebookLeadInteraction(item) { + const subject = String(item?.subject || item?.title || '').trim().toLowerCase(); + if (subject.startsWith('[facebook]')) { + return true; + } + + const customerId = item?.customer_id || item?.customerId || ''; + if (!customerId) { + return false; + } + const customer = state.customers.items.find((entry) => entry.customer_id === customerId) || null; + return isFacebookCustomer(customer); +} + +function customerInitials(name) { + const parts = String(name || '') + .trim() + .split(/\s+/) + .filter(Boolean) + .slice(0, 2); + if (!parts.length) { + return 'LD'; + } + return parts.map((part) => part[0].toUpperCase()).join(''); +} + +function customerLeadSourceMeta(item, index) { + if (isFacebookCustomer(item)) { + return { label: 'Facebook', className: 'lead-badge source-facebook' }; + } + const variants = [ + { label: 'Instagram', className: 'lead-badge source-instagram' }, + { label: 'WhatsApp', className: 'lead-badge source-whatsapp' }, + { label: 'Site', className: 'lead-badge source-site' }, + { label: 'Referral', className: 'lead-badge source-referral' }, + ]; + return variants[customerHashSeed(item, index) % variants.length]; +} + +function customerLeadStatusMeta(item, index) { + if (isFacebookCustomer(item)) { + return { label: 'Новый', className: 'lead-badge status-new' }; + } + const variants = [ + { label: 'Переговоры', className: 'lead-badge status-negotiation' }, + { label: 'Квалификация', className: 'lead-badge status-qualification' }, + { label: 'Предложение', className: 'lead-badge status-proposal' }, + { label: 'Первичный контакт', className: 'lead-badge status-initial' }, + { label: 'Успех', className: 'lead-badge status-success' }, + { label: 'Новый', className: 'lead-badge status-new' }, + { label: 'Отказ', className: 'lead-badge status-lost' }, + ]; + return variants[customerHashSeed(item, index + 17) % variants.length]; +} + +function customerLeadScore(item, index) { + return 12 + (customerHashSeed(item, index + 29) % 81); +} + +function customerLeadScoreClass(score) { + if (score >= 80) { + return 'lead-score score-strong'; + } + if (score >= 50) { + return 'lead-score score-medium'; + } + return 'lead-score score-weak'; +} + +function customerLeadCompany(item) { + const tags = (Array.isArray(item?.tags) ? item.tags : []) + .map((tag) => String(tag || '').trim()) + .filter((tag) => tag && !['facebook', 'facebook-lead', 'hot-lead'].includes(tag.toLowerCase())); + if (tags.length) { + return tags[0]; + } + const suffixes = ['Tech', 'Group', 'Retail', 'Logistics', 'Studio', 'Services']; + const seed = customerHashSeed(item, 41); + return `${String(item?.display_name || 'Lead').split(' ')[0]} ${suffixes[seed % suffixes.length]}`; +} + +function customerLeadCreatedAtLabel(item, index) { + if (item?.created_at) { + return formatIsoShort(item.created_at); + } + const seed = customerHashSeed(item, index + 59); + const shifted = new Date(Date.now() - ((seed % 9) * 86400000 + (seed % 18) * 3600000)); + return shifted.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric' }); +} + +function customerLeadRows() { + const start = (state.customers.page - 1) * state.customers.pageSize; + return state.customers.items.slice(start, start + state.customers.pageSize); +} + +function selectedCustomer() { + return state.customers.items.find((item) => item.customer_id === state.customers.selectedCustomerId) || state.customers.items[0] || null; +} + +function customerProfileHash(customerId = state.customers.selectedCustomerId) { + return customerId ? `#customer-profile/${encodeURIComponent(customerId)}` : '#customer-profile'; +} + +function openCustomerProfile(customerId) { + selectCustomer(customerId || ''); + ensureCustomerHistoryLoaded(customerId || state.customers.selectedCustomerId, { force: true }).catch(() => {}); + window.location.hash = customerProfileHash(customerId || state.customers.selectedCustomerId); +} + +function interactionById(interactionId) { + return state.interactions.find((item) => item.interaction_id === interactionId) || null; +} + +function customerIdForInteractionId(interactionId) { + return interactionById(interactionId)?.customer_id || ''; +} + +function customerDisplayName(customerId) { + if (!customerId) { + return 'не привязан'; + } + const customer = state.customers.items.find((item) => item.customer_id === customerId) || null; + return customer?.display_name || customerId; +} + +function customerIndex(customer) { + if (!customer) { + return -1; + } + return state.customers.items.findIndex((item) => item.customer_id === customer.customer_id); +} + +function customerPhones(customer) { + const phones = []; + if (Array.isArray(customer?.phones)) { + phones.push(...customer.phones); + } + if (customer?.phone) { + phones.push(customer.phone); + } + if (customer?.preferred_phone) { + phones.unshift(customer.preferred_phone); + } + return [...new Set(phones.map((value) => String(value || '').trim()).filter(Boolean))]; +} + +function customerInteractions(customer) { + if (!customer) { + return []; + } + return state.interactions.filter((item) => item.customer_id === customer.customer_id); +} + +function customerInteractionIds(customer) { + return new Set(customerInteractions(customer).map((item) => item.interaction_id).filter(Boolean)); +} + +function customerTelegramThreads(customer) { + const interactionIds = customerInteractionIds(customer); + return state.telegram.threads.filter((thread) => interactionIds.has(thread.interaction_id)); +} + +function customerLiveCalls(customer) { + const interactionIds = customerInteractionIds(customer); + const phones = new Set(customerPhones(customer)); + return uniqueLiveCalls([...state.liveCalls.items, ...state.liveCalls.recentItems]).filter((item) => { + if (interactionIds.has(item.interaction_id)) { + return true; + } + return phones.has(String(item.caller_number || '').trim()); + }); +} + +function customerHistoryPayload(customerId) { + if (!customerId) { + return null; + } + return state.customers.historyById[customerId] || null; +} + +function customerHistoryPending(customerId) { + return Boolean(customerId && state.customers.pendingHistoryById[customerId]); +} + +async function ensureCustomerHistoryLoaded(customerId, options = {}) { + const { force = false } = options; + if (!customerId) { + return null; + } + if (customerHistoryPending(customerId)) { + return customerHistoryPayload(customerId); + } + if (!force && customerHistoryPayload(customerId)) { + return customerHistoryPayload(customerId); + } + state.customers.pendingHistoryById[customerId] = true; + delete state.customers.historyErrorsById[customerId]; + try { + const data = await api('customer', `customers/${encodeURIComponent(customerId)}/history`); + state.customers.historyById[customerId] = data || null; + return state.customers.historyById[customerId]; + } catch (err) { + state.customers.historyErrorsById[customerId] = err.message; + return null; + } finally { + delete state.customers.pendingHistoryById[customerId]; + renderCustomerProfilePage(); + } +} + +function customerPreferredChannel(customer) { + const counters = new Map(); + customerInteractions(customer).forEach((item) => { + const key = item.channel || 'voice'; + counters.set(key, (counters.get(key) || 0) + 1); + }); + if (customerTelegramThreads(customer).length) { + counters.set('telegram', (counters.get('telegram') || 0) + customerTelegramThreads(customer).length); + } + if (customerLiveCalls(customer).length) { + counters.set('voice', (counters.get('voice') || 0) + customerLiveCalls(customer).length); + } + const [winner] = [...counters.entries()].sort((left, right) => right[1] - left[1]); + return winner?.[0] || 'voice'; +} + +function customerHistoryTimeValue(value) { + if (!value) { + return 0; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return 0; + } + return dt.getTime(); +} + +function customerHistoryMeta(kind) { + if (kind === 'voice') { + return { token: 'VC', className: 'voice' }; + } + if (kind === 'telegram') { + return { token: 'TG', className: 'telegram' }; + } + if (kind === 'email') { + return { token: 'EM', className: 'email' }; + } + if (kind === 'webchat') { + return { token: 'WC', className: 'webchat' }; + } + if (kind === 'profile') { + return { token: 'ID', className: 'profile' }; + } + return { token: 'CS', className: 'case' }; +} + +function buildCustomerHistoryEvents(customer) { + if (!customer) { + return []; + } + + const company = customerLeadCompany(customer); + const phones = customerPhones(customer); + const interactions = customerInteractions(customer); + const interactionIds = new Set(interactions.map((item) => item.interaction_id).filter(Boolean)); + const threads = customerTelegramThreads(customer); + const liveCalls = customerLiveCalls(customer); + const events = []; + + events.push({ + timestamp: customer.created_at || '', + kind: 'profile', + title: 'Профиль клиента активен', + body: phones.length + ? `Контакт доступен для оператора по номеру ${phones[0]}.` + : 'Контакт добавлен в клиентскую базу и готов к работе.', + note: [customer.customer_id, company].filter(Boolean).join(' • '), + }); + + interactions.forEach((item) => { + const meta = statusMeta(item.status); + events.push({ + timestamp: item.updated_at || item.created_at || '', + kind: item.channel || 'case', + title: `${channelLabel(item.channel)} • ${meta.label}`, + body: item.subject || 'Обращение без темы', + note: [ + item.interaction_id, + item.assigned_to ? `владелец ${item.assigned_to}` : '', + item.queue_id ? `queue ${item.queue_id}` : '', + ].filter(Boolean).join(' • '), + interaction_id: item.interaction_id, + }); + }); + + threads.forEach((thread) => { + events.push({ + timestamp: thread.last_message_at || '', + kind: 'telegram', + title: 'Telegram диалог', + body: thread.last_message_preview || 'Последнее сообщение недоступно.', + note: [ + thread.display_name || thread.username || `chat ${thread.chat_id}`, + thread.claimed_by_user ? `владелец ${thread.claimed_by_user}` : '', + thread.status ? statusMeta(thread.status).label : '', + ].filter(Boolean).join(' • '), + interaction_id: thread.interaction_id, + thread_id: thread.thread_id, + }); + }); + + const activeThread = selectedTelegramThread(); + if (activeThread && interactionIds.has(activeThread.interaction_id) && state.telegram.messages.length) { + state.telegram.messages.slice(-3).forEach((message) => { + const direction = message.direction === 'outbound' ? 'Исходящее сообщение' : 'Входящее сообщение'; + events.push({ + timestamp: message.created_at || '', + kind: 'telegram', + title: direction, + body: message.text || 'Сообщение без текста', + note: [ + activeThread.display_name || activeThread.username || `chat ${activeThread.chat_id}`, + message.operator_user ? `оператор ${message.operator_user}` : '', + message.delivery_status || '', + ].filter(Boolean).join(' • '), + interaction_id: activeThread.interaction_id, + thread_id: activeThread.thread_id, + }); + }); + } + + liveCalls.forEach((item) => { + const completed = item.telephony_status === 'ended' || item.ended_at; + events.push({ + timestamp: item.last_transition_at || item.ended_at || item.connected_at || item.started_at || '', + kind: 'voice', + title: completed ? `Звонок • ${terminalActionLabel(item)}` : `Звонок • ${telephonyLabel(item.telephony_status)}`, + body: `Номер клиента: ${item.caller_number || 'не определён'}`, + note: [ + item.call_id, + item.operator_extension ? `внутр. ${item.operator_extension}` : '', + item.claimed_by_user ? `владелец ${item.claimed_by_user}` : '', + ].filter(Boolean).join(' • '), + interaction_id: item.interaction_id, + call_id: item.call_id, + }); + const aiMeta = voiceAiStatusMeta(item); + if (aiMeta) { + events.push({ + timestamp: item.ai_last_model_at || item.last_transition_at || item.updated_at || '', + kind: 'voice', + title: `Голосовой AI • ${aiMeta.label}`, + body: item.ai_handoff_reason || 'AI участвовал в обработке звонка и обновил контекст для оператора.', + note: [ + item.call_id, + item.voice_session_id ? `голос ${item.voice_session_id}` : '', + item.ai_session_id ? `ai ${item.ai_session_id}` : '', + ].filter(Boolean).join(' • '), + interaction_id: item.interaction_id, + call_id: item.call_id, + }); + } + }); + + return events + .sort((left, right) => { + const timeDelta = customerHistoryTimeValue(right.timestamp) - customerHistoryTimeValue(left.timestamp); + if (timeDelta !== 0) { + return timeDelta; + } + return String(right.note || '').localeCompare(String(left.note || '')); + }) + .slice(0, 9); +} + +function latestItemByTime(items, fields = []) { + const source = Array.isArray(items) ? items.filter(Boolean) : []; + if (!source.length) { + return null; + } + return [...source].sort((left, right) => { + const rightTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(right?.[field])), 0); + const leftTime = fields.reduce((max, field) => Math.max(max, customerHistoryTimeValue(left?.[field])), 0); + if (rightTime !== leftTime) { + return rightTime - leftTime; + } + return String(right?.interaction_id || right?.thread_id || right?.call_id || '').localeCompare( + String(left?.interaction_id || left?.thread_id || left?.call_id || ''), + ); + })[0] || null; +} + +function customerHistoryEventActions(event, context = {}) { + const threadIds = context.threadIds instanceof Set ? context.threadIds : new Set(); + const callIds = context.callIds instanceof Set ? context.callIds : new Set(); + const actions = []; + if (event?.thread_id && threadIds.has(event.thread_id)) { + actions.push({ + action: 'telegram', + label: 'К Telegram', + threadId: event.thread_id, + }); + } + if (event?.call_id && callIds.has(event.call_id)) { + actions.push({ + action: 'call', + label: 'К звонку', + callId: event.call_id, + }); + } + if (event?.interaction_id) { + actions.push({ + action: 'interaction', + label: 'К кейсу', + interactionId: event.interaction_id, + }); + } + return actions; +} + +function renderCustomerHistoryActions(event, context = {}) { + const actions = customerHistoryEventActions(event, context); + if (!actions.length) { + return ''; + } + return ` +
            + ${actions.map((action) => ` + + `).join('')} +
            + `; +} + +function emptyCustomerSpotlightMarkup(title, description) { + return ` +
            + ${escapeHtml(title)} +

            ${escapeHtml(description)}

            +
            + `; +} + +function customerProfileVoiceNameContext(liveCalls = []) { + const latestCall = latestItemByTime(liveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']); + if (!latestCall) { + return { + statusMeta: null, + sourceLabel: '', + }; + } + const voiceContext = voiceSummaryForItem(latestCall) || latestCall || {}; + return { + statusMeta: voiceCustomerNameStatusMeta(voiceContext?.customer_name_status), + sourceLabel: formatVoiceCustomerNameSource(voiceContext?.customer_name_source), + }; +} + +function customerProfileNameEditorMarkup(customer, context = {}) { + const editor = state.customers.nameEditor; + const isOpen = editor.open && editor.customerId === customer.customer_id; + const statusMeta = context.statusMeta || null; + const sourceLabel = String(context.sourceLabel || '').trim(); + const flash = editor.flash && editor.customerId === customer.customer_id ? editor.flash : ''; + const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : 'Имя сохранится в профиле клиента и voice-контуре.'); + return ` +
            +
            +

            ${escapeHtml(customer.display_name || 'Без имени')}

            +

            ${escapeHtml(customerLeadCompany(customer))}

            + ${(statusMeta || sourceLabel || flash) ? ` +
            + ${statusMeta ? `${escapeHtml(statusMeta.shortLabel)}` : ''} + ${sourceLabel ? `Источник: ${escapeHtml(sourceLabel)}` : ''} + ${flash ? `${escapeHtml(flash)}` : ''} +
            + ` : ''} +
            + +
            + ${isOpen ? ` +
            +
            Как сохранить имя клиента в профиле
            +
            + + + +
            +

            ${escapeHtml(statusText)}

            +
            + ` : ''} + `; +} + +function customerSpotlightMarkup(customer, options = {}) { + const { pageView = false } = options; + const index = Math.max(customerIndex(customer), 0); + const phones = customerPhones(customer); + const source = customerLeadSourceMeta(customer, index); + const leadStatus = customerLeadStatusMeta(customer, index); + const score = customerLeadScore(customer, index); + const historyPayload = customerHistoryPayload(customer.customer_id); + const historySummary = historyPayload?.summary || null; + const currentThreads = customerTelegramThreads(customer); + const currentLiveCalls = customerLiveCalls(customer); + const interactions = Array.isArray(historyPayload?.interactions) + ? historyPayload.interactions + : customerInteractions(customer); + const threads = Array.isArray(historyPayload?.telegram_threads) + ? historyPayload.telegram_threads + : customerTelegramThreads(customer); + const liveCalls = Array.isArray(historyPayload?.live_calls) + ? historyPayload.live_calls + : customerLiveCalls(customer); + const profileNameContext = customerProfileVoiceNameContext(liveCalls); + const historyEvents = Array.isArray(historyPayload?.history) && historyPayload.history.length + ? historyPayload.history + : buildCustomerHistoryEvents(customer); + const latestEvent = historyEvents[0] || null; + const openCases = Number.isFinite(Number(historySummary?.open_cases)) + ? Number(historySummary.open_cases) + : interactions.filter((item) => item.status !== 'closed').length; + const activeChannels = Array.isArray(historySummary?.active_channels) && historySummary.active_channels.length + ? historySummary.active_channels + : [...new Set([ + ...interactions.map((item) => item.channel).filter(Boolean), + ...(threads.length ? ['telegram'] : []), + ...(liveCalls.length ? ['voice'] : []), + ])]; + const tags = Array.isArray(customer.tags) ? customer.tags.filter(Boolean) : []; + const latestInteraction = latestItemByTime(interactions, ['updated_at', 'created_at']); + const primaryThread = historySummary?.primary_telegram_thread_id + ? currentThreads.find((item) => item.thread_id === historySummary.primary_telegram_thread_id) || null + : latestItemByTime(currentThreads, ['last_message_at', 'updated_at', 'created_at']); + const primaryCall = latestItemByTime(currentLiveCalls, ['last_transition_at', 'ended_at', 'connected_at', 'started_at', 'updated_at']); + const historyActionContext = { + threadIds: new Set(currentThreads.map((item) => item.thread_id).filter(Boolean)), + callIds: new Set(currentLiveCalls.map((item) => item.call_id).filter(Boolean)), + }; + const historyStatusText = customerHistoryPending(customer.customer_id) + ? 'Обновляем ленту клиента из backend...' + : state.customers.historyErrorsById[customer.customer_id] + ? 'Показываем локальную историю, пока backend недоступен.' + : (latestEvent ? `Последнее событие: ${latestEvent.title}` : 'События появятся после первого обращения'); + const summaryCards = [ + renderSummaryCard( + 'Контакты', + String(Number.isFinite(Number(historySummary?.contact_points)) ? Number(historySummary.contact_points) : (interactions.length + threads.length + liveCalls.length)), + 'Все точки касания клиента', + ), + renderSummaryCard('Открытые кейсы', String(openCases), openCases ? 'Требуют внимания оператора' : 'Новых действий нет'), + renderSummaryCard( + 'Каналы', + String(activeChannels.length || (phones.length ? 1 : 0)), + activeChannels.length ? activeChannels.map((item) => channelLabel(item)).join(' • ') : 'Пока только профиль', + ), + renderSummaryCard( + 'Последний контакт', + historySummary?.latest_event_at ? formatIsoShort(historySummary.latest_event_at) : (latestEvent ? formatIsoShort(latestEvent.timestamp) : '—'), + historySummary?.latest_event_title || latestEvent?.title || 'Активность ещё не зафиксирована', + ), + ].join(''); + + const historyMarkup = historyEvents.length + ? historyEvents.map((event) => { + const meta = customerHistoryMeta(event.kind); + return ` +
            +
            ${meta.token}
            +
            +
            +
            ${escapeHtml(event.title)}
            + +
            +
            ${escapeHtml(event.body)}
            + ${event.note ? `
            ${escapeHtml(event.note)}
            ` : ''} + ${renderCustomerHistoryActions(event, historyActionContext)} +
            +
            + `; + }).join('') + : '
            Пока нет событий по этому клиенту.
            '; + + return ` +
            +
            +
            +
            ${escapeHtml(customerInitials(customer.display_name))}
            +
            +
            Клиент ${escapeHtml(customer.customer_id)}
            + ${customerProfileNameEditorMarkup(customer, profileNameContext)} +
            +
            + ${escapeHtml(source.label)} + ${escapeHtml(leadStatus.label)} + AI ${score} +
            +
            + +
            + ${summaryCards} +
            + +
            +
            +
            Основной номер
            +
            ${escapeHtml(historySummary?.primary_phone || phones[0] || 'Не указан')}
            +
            ${phones.length > 1 ? `+${phones.length - 1} дополнительных номера` : 'Один контактный номер'}
            +
            +
            +
            Предпочтительный канал
            +
            ${escapeHtml(channelLabel(customerPreferredChannel(customer)))}
            +
            ${escapeHtml(openCases ? 'Есть активные обращения' : 'Сейчас без открытых кейсов')}
            +
            +
            +
            Теги профиля
            +
            + ${(tags.length ? tags : [customerLeadCompany(customer)]).slice(0, 4).map((tag) => `${escapeHtml(tag)}`).join('')} +
            +
            +
            +
            Активные каналы
            +
            + ${(activeChannels.length ? activeChannels : ['profile']).map((channel) => `${escapeHtml(channel === 'profile' ? 'Профиль' : channelLabel(channel))}`).join('')} +
            +
            +
            + +
            + + + + +
            +
            + +
            +
            +
            +
            Единая история клиента
            +

            Все контакты в одной ленте

            +
            +
            ${escapeHtml(historyStatusText)}
            +
            +
            + ${historyMarkup} +
            +
            +
            + `; +} + +function renderCustomerSpotlight() { + const target = $('customerSpotlight'); + const customer = selectedCustomer(); + if (target && !customer) { + target.innerHTML = emptyCustomerSpotlightMarkup( + 'Выберите клиента', + 'Здесь появятся профиль клиента и единая история контактов по всем каналам.', + ); + } + if (target && customer) { + const listEl = target.querySelector('.customer-history-list'); + const scrollPos = listEl ? listEl.scrollTop : 0; + const h = target.offsetHeight; + if (h > 0) target.style.minHeight = h + 'px'; + + target.innerHTML = customerSpotlightMarkup(customer); + + if (h > 0) target.style.minHeight = ''; + const newListEl = target.querySelector('.customer-history-list'); + if (newListEl && scrollPos > 0) { + newListEl.scrollTop = scrollPos; + } + } + renderCustomerProfilePage(); +} + +function renderCustomerProfilePage() { + const target = $('customerProfileContent'); + if (!target) { + return; + } + + const customer = selectedCustomer(); + const title = $('customerProfileTitle'); + const hint = $('customerProfileHint'); + + if (!customer) { + if (title) { + title.textContent = 'Профиль клиента'; + } + if (hint) { + hint.textContent = 'Откройте карточку из списка клиентов, чтобы увидеть профиль и историю.'; + } + target.innerHTML = emptyCustomerSpotlightMarkup( + 'Выберите клиента', + 'Откройте карточку из списка клиентов, чтобы увидеть профиль и историю.', + ); + return; + } + + if (title) { + title.textContent = customer.display_name || 'Профиль клиента'; + } + const historyPayload = customerHistoryPayload(customer.customer_id); + ensureCustomerHistoryLoaded(customer.customer_id).catch(() => {}); + if (hint) { + const phones = customerPhones(customer); + const primaryPhone = historyPayload?.summary?.primary_phone || phones[0] || ''; + hint.textContent = primaryPhone + ? `Клиент ${customer.customer_id} • основной номер ${primaryPhone}` + : `Клиент ${customer.customer_id} • омниканальный профиль`; + } + + const listEl = target.querySelector('.customer-history-list'); + const scrollPos = listEl ? listEl.scrollTop : 0; + const pageScroll = window.scrollY || document.documentElement.scrollTop; + const h = target.offsetHeight; + if (h > 0) target.style.minHeight = h + 'px'; + + target.innerHTML = customerSpotlightMarkup(customer, { pageView: true }); + + if (h > 0) target.style.minHeight = ''; + const newListEl = target.querySelector('.customer-history-list'); + if (newListEl && scrollPos > 0) { + newListEl.scrollTop = scrollPos; + } + if (pageScroll > 0 && pageScroll > window.scrollY) { + window.scrollTo(window.scrollX, pageScroll); + } +} + +function syncCustomerFormUi() { + $('customerLeadForm').classList.toggle('hidden', !state.customers.leadFormOpen); + $('toggleCustomerLeadFormBtn').textContent = state.customers.leadFormOpen ? 'Закрыть форму' : '+ Добавить лид'; +} + +function updateCustomerPagerUi() { + const total = state.customers.items.length; + const pageCount = Math.max(1, Math.ceil(total / state.customers.pageSize)); + const start = total ? (state.customers.page - 1) * state.customers.pageSize + 1 : 0; + const end = total ? Math.min(state.customers.page * state.customers.pageSize, total) : 0; + $('customerPageSummary').textContent = total ? `Показано ${start}-${end} из ${total}` : 'Пока нет лидов.'; + $('customerPrevPageBtn').disabled = state.customers.page <= 1; + $('customerNextPageBtn').disabled = state.customers.page >= pageCount; +} + +function renderCustomerList() { + const rows = customerLeadRows(); + if (!rows.length) { + $('customerList').innerHTML = ` +
            + Лиды пока не найдены. +

            Попробуйте другой запрос или добавьте новый лид вручную.

            +
            + `; + renderCustomerSpotlight(); + updateCustomerPagerUi(); + return; + } + + $('customerList').innerHTML = ` +
            +
            +
            +
            ИМЯ КОНТАКТА
            +
            ИСТОЧНИК
            +
            ДАТА СОЗДАНИЯ
            +
            СТАТУС
            +
            AI SCORE
            +
            + ${rows.map((item, index) => { + const source = customerLeadSourceMeta(item, index); + const status = customerLeadStatusMeta(item, index); + const score = customerLeadScore(item, index); + const selected = item.customer_id === state.customers.selectedCustomerId ? ' selected' : ''; + return ` +
            +
            +
            +
            ${escapeHtml(customerInitials(item.display_name))}
            +
            +
            ${escapeHtml(item.display_name)}
            +
            ${escapeHtml(customerLeadCompany(item))}
            +
            +
            +
            ${escapeHtml(source.label)}
            +
            ${escapeHtml(customerLeadCreatedAtLabel(item, index))}
            +
            ${escapeHtml(status.label)}
            +
            ${score}
            +
            + `; + }).join('')} +
            + `; + renderCustomerSpotlight(); + updateCustomerPagerUi(); +} + +function selectCustomer(customerId) { + if (state.customers.nameEditor.customerId && state.customers.nameEditor.customerId !== customerId) { + state.customers.nameEditor.open = false; + state.customers.nameEditor.customerId = ''; + state.customers.nameEditor.draft = ''; + state.customers.nameEditor.saving = false; + state.customers.nameEditor.error = ''; + state.customers.nameEditor.flash = ''; + } + state.customers.selectedCustomerId = customerId || ''; + if (customerId) { + $('interactionCustomerId').value = customerId; + } + renderCustomerList(); +} + +function setCustomerPage(page) { + const totalPages = Math.max(1, Math.ceil(state.customers.items.length / state.customers.pageSize)); + state.customers.page = Math.min(Math.max(page, 1), totalPages); + renderCustomerList(); +} + +function selectedTelegramThread() { + return state.telegram.threads.find((thread) => thread.thread_id === state.telegram.selectedThreadId) || null; +} + +function telegramThreadCanClaim(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + return !thread.claimed_by_user; +} + +function telegramThreadCanReply(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + if (state.role === 'admin' || state.role === 'supervisor') { + return true; + } + return thread.claimed_by_user === state.user; +} + +function telegramThreadCanManage(thread) { + if (!thread) { + return false; + } + if (state.role === 'admin' || state.role === 'supervisor') { + return true; + } + return thread.claimed_by_user === state.user; +} + +function telegramThreadCanReturnToAi(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + if (String(thread.ai_state || '') !== 'human_owned') { + return false; + } + return telegramThreadCanManage(thread); +} + +function telegramThreadDisplayName(thread) { + return thread?.display_name || thread?.username || thread?.chat_id || 'Telegram'; +} + +function telegramThreadInitials(thread) { + return customerInitials(telegramThreadDisplayName(thread)); +} + +function telegramThreadTimeLabel(value) { + if (!value) { + return 'сейчас'; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return String(value); + } + const now = new Date(); + const sameDay = dt.toDateString() === now.toDateString(); + if (sameDay) { + return dt.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + } + return dt.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short' }); +} + +function telegramHashValue(value) { + let hash = 0; + const text = String(value || 'telegram'); + for (let index = 0; index < text.length; index += 1) { + hash = ((hash << 5) - hash + text.charCodeAt(index)) >>> 0; + } + return hash; +} + +function telegramAvatarPalette(seed) { + return TELEGRAM_AVATAR_PALETTES[telegramHashValue(seed) % TELEGRAM_AVATAR_PALETTES.length]; +} + +function telegramAvatarStyle(seed) { + const [start, end] = telegramAvatarPalette(seed); + return `style="--telegram-avatar-start:${start};--telegram-avatar-end:${end};"`; +} + +function whatsappAvatarPalette(seed) { + return WHATSAPP_AVATAR_PALETTES[telegramHashValue(seed || 'whatsapp') % WHATSAPP_AVATAR_PALETTES.length]; +} + +function cloneWhatsappChats() { + return WHATSAPP_MOCK_CHATS.map((chat) => ({ + ...chat, + messages: (chat.messages || []).map((message) => ({ ...message })), + })); +} + +function sortWhatsappChats(chats) { + return [...(chats || [])].sort((left, right) => { + const pinnedDelta = Number(Boolean(right.pinned)) - Number(Boolean(left.pinned)); + if (pinnedDelta !== 0) { + return pinnedDelta; + } + return String(right.lastMessageAt || '').localeCompare(String(left.lastMessageAt || '')); + }); +} + +function seedWhatsappChats() { + if (state.whatsapp.chats.length) { + return; + } + state.whatsapp.mode = 'mock'; + state.whatsapp.chats = sortWhatsappChats(cloneWhatsappChats()); + state.whatsapp.selectedChatId = state.whatsapp.chats[0]?.id || ''; +} + +function whatsappChatThreadId(chat) { + return chat?.threadId || chat?.id || ''; +} + +function whatsappThreadCanClaim(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + return !thread.claimedByUser; +} + +function whatsappThreadCanReply(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + if (state.role === 'admin' || state.role === 'supervisor') { + return true; + } + return !thread.claimedByUser || thread.claimedByUser === state.user; +} + +function whatsappThreadCanManage(thread) { + if (!thread) { + return false; + } + if (state.role === 'admin' || state.role === 'supervisor') { + return true; + } + return thread.claimedByUser === state.user; +} + +function whatsappThreadCanReturnToAi(thread) { + if (!thread || thread.status === 'closed') { + return false; + } + if (String(thread.aiState || '').trim() !== 'human_owned') { + return false; + } + return whatsappThreadCanManage(thread); +} + +function whatsappThreadDisplayName(thread) { + return thread?.title || thread?.display_name || thread?.phoneNumber || thread?.phone_number || thread?.chatId || thread?.chat_id || 'WhatsApp'; +} + +function whatsappThreadAiStateMeta(thread) { + const stateKey = String(thread?.aiState || thread?.ai_state || '').trim(); + if (!stateKey) { + return null; + } + const meta = { + queued: { label: 'AI готов', tone: 'queued' }, + thinking: { label: 'AI думает', tone: 'thinking' }, + active: { label: 'AI активен', tone: 'active' }, + handoff_required: { label: 'Нужен оператор', tone: 'handoff' }, + human_owned: { label: 'Operator', tone: 'human' }, + closed: { label: 'AI завершён', tone: 'closed' }, + error: { label: 'Ошибка AI', tone: 'error' }, + }; + return meta[stateKey] || { label: stateKey, tone: 'queued' }; +} + +function whatsappThreadAiClaimable(thread) { + return Boolean(thread && !thread.claimedByUser && ['queued', 'thinking', 'active', 'handoff_required'].includes(String(thread.aiState || ''))); +} + +function whatsappThreadUnreadCount(thread) { + const raw = Number(thread?.unreadCount ?? thread?.unread_count ?? 0); + return Number.isFinite(raw) && raw > 0 ? raw : 0; +} + +function whatsappThreadHandle(thread) { + if (!thread) { + return 'Direct message'; + } + if (thread.handle) { + return thread.handle; + } + if (thread.phoneNumber || thread.phone_number) { + return thread.phoneNumber || thread.phone_number; + } + if (thread.whatsappUserId || thread.whatsapp_user_id) { + return thread.whatsappUserId || thread.whatsapp_user_id; + } + if (thread.isGroup || thread.is_group) { + return 'Групповой чат'; + } + const chatId = thread.chatId || thread.chat_id; + return chatId ? `chat ${chatId}` : 'Direct message'; +} + +function whatsappThreadPresenceText(thread) { + if (!thread) { + return 'Выберите чат'; + } + if (thread.status === 'closed') { + return 'Закрыт'; + } + const aiMeta = whatsappThreadAiStateMeta(thread); + if (aiMeta && String(thread.aiState || '').trim() !== 'human_owned') { + return aiMeta.label; + } + if (thread.claimedByUser) { + return `в работе у ${thread.claimedByUser}`; + } + return thread.statusLine || whatsappThreadHandle(thread); +} + +function whatsappThreadOwnerText(thread) { + if (whatsappThreadAiClaimable(thread)) { + return 'владелец AI'; + } + return thread?.claimedByUser ? `владелец ${thread.claimedByUser}` : 'владелец -'; +} + +function whatsappThreadSummaryAccessible(thread) { + if (!thread) { + return false; + } + if (!['handoff_required', 'human_owned'].includes(String(thread.aiState || '').trim())) { + return false; + } + if (String(state.role || '') !== 'operator') { + return true; + } + const claimedByUser = String(thread.claimedByUser || '').trim(); + return !claimedByUser || claimedByUser === String(state.user || '').trim(); +} + +function whatsappSelectedSummaryVisible(thread) { + if (!whatsappThreadSummaryAccessible(thread)) { + return false; + } + const summary = state.whatsapp.selectedThreadSummary; + return Boolean(summary && summary.thread_id === whatsappChatThreadId(thread)); +} + +function whatsappSummaryFieldMarkup(label, value) { + return ` +
            +
            ${escapeHtml(label)}
            +
            ${escapeHtml(value || '-')}
            +
            + `; +} + +function whatsappSummaryStatusMarkup(summary) { + if (!summary?.status_label) { + return ''; + } + const tone = String(summary.status_tone || 'handoff').trim() || 'handoff'; + return `${escapeHtml(summary.status_label)}`; +} + +function normalizeWhatsappDeliveryStatus(status) { + const raw = String(status || '').trim().toLowerCase(); + if (!raw) { + return ''; + } + if (raw.includes('read')) { + return 'read'; + } + if (raw.includes('deliver') || raw.includes('sent')) { + return 'delivered'; + } + return raw; +} + +function whatsappAuthorLabel(message, chat = null) { + if (message.author) { + return message.author; + } + const payload = message.payload || {}; + if (message.author_type === 'customer') { + return payload.display_name || payload.profile_name || chat?.title || ''; + } + if (message.author_type === 'human') { + return message.operator_user || payload.operator_user || 'Оператор'; + } + if (message.author_type === 'ai') { + return 'AI'; + } + return ''; +} + +function normalizeWhatsappMessage(message, chat = null) { + const direction = message.direction === 'outbound' + ? 'out' + : message.direction === 'inbound' + ? 'in' + : (message.direction || 'system'); + return { + id: message.message_id || message.id, + direction, + text: message.text || '', + createdAt: message.created_at || message.createdAt || '', + deliveryStatus: normalizeWhatsappDeliveryStatus(message.delivery_status || message.deliveryStatus), + author: whatsappAuthorLabel(message, chat), + authorType: message.author_type || message.authorType || '', + operatorUser: message.operator_user || '', + payload: message.payload || {}, + }; +} + +function whatsappThreadStatusLine(thread) { + const aiMeta = whatsappThreadAiStateMeta(thread); + if (aiMeta && String(thread.ai_state || '').trim() !== 'human_owned') { + return aiMeta.label; + } + if (thread.claimed_by_user) { + return `owned by ${thread.claimed_by_user}`; + } + return thread.phone_number || thread.whatsapp_user_id || (thread.is_group ? 'Групповой чат' : 'Чат WhatsApp'); +} + +function normalizeWhatsappThread(thread, existingChat = null) { + return { + id: thread.thread_id, + threadId: thread.thread_id, + interactionId: thread.interaction_id, + chatId: thread.chat_id, + whatsappUserId: thread.whatsapp_user_id || '', + phoneNumber: thread.phone_number || '', + title: thread.display_name || thread.phone_number || thread.chat_id || 'WhatsApp', + handle: thread.phone_number || thread.whatsapp_user_id || (thread.is_group ? 'Групповой чат' : `чат ${thread.chat_id}`), + statusLine: whatsappThreadStatusLine(thread), + isGroup: Boolean(thread.is_group), + unreadCount: whatsappThreadUnreadCount(thread), + pinned: Boolean(existingChat?.pinned), + muted: Boolean(existingChat?.muted), + lastMessageAt: thread.last_message_at || '', + lastMessagePreview: thread.last_message_preview || '', + claimedByUser: thread.claimed_by_user || '', + claimedAt: thread.claimed_at || '', + status: thread.status || '', + queueId: thread.queue_id || '', + aiSessionId: thread.ai_session_id || '', + aiState: thread.ai_state || '', + aiHandoffReason: thread.ai_handoff_reason || '', + aiLastModelAt: thread.ai_last_model_at || '', + createdAt: thread.created_at || '', + updatedAt: thread.updated_at || '', + source: 'live', + messages: Array.isArray(existingChat?.messages) ? existingChat.messages.map((message) => ({ ...message })) : [], + }; +} + +function applyWhatsappChats(chats, options = {}) { + const { preserveSelection = true } = options; + const sorted = sortWhatsappChats(chats); + const previous = preserveSelection ? state.whatsapp.selectedChatId : ''; + const stillExists = sorted.some((chat) => chat.id === previous); + const nextSelected = stillExists ? previous : (sorted[0]?.id || ''); + state.whatsapp.chats = sorted; + state.whatsapp.selectedChatId = nextSelected; + if (nextSelected !== previous) { + state.whatsapp.composerText = ''; + state.whatsapp.selectedThreadSummary = null; + } + if (!nextSelected) { + state.whatsapp.selectedThreadSummary = null; + } + renderWhatsappWorkspace(); + return nextSelected; +} + +function upsertWhatsappThreadData(thread, options = {}) { + const existing = state.whatsapp.chats.find((item) => item.id === thread.thread_id) || null; + const normalized = normalizeWhatsappThread(thread, existing); + const nextChats = existing + ? state.whatsapp.chats.map((item) => (item.id === normalized.id ? normalized : item)) + : [...state.whatsapp.chats, normalized]; + applyWhatsappChats(nextChats, options); + return normalized; +} + +function whatsappEstimateUnreadCount(messages) { + let lastOutboundIndex = -1; + (messages || []).forEach((message, index) => { + if (message.direction === 'out') { + lastOutboundIndex = index; + } + }); + return (messages || []) + .slice(lastOutboundIndex + 1) + .filter((message) => message.direction === 'in') + .length; +} + +function updateWhatsappChatMessages(threadId, messages, options = {}) { + const { preserveUnread = false } = options; + const nextChats = state.whatsapp.chats.map((chat) => { + if (chat.id !== threadId) { + return chat; + } + return { + ...chat, + messages, + unreadCount: preserveUnread ? chat.unreadCount : (threadId === state.whatsapp.selectedChatId ? 0 : whatsappEstimateUnreadCount(messages)), + }; + }); + applyWhatsappChats(nextChats); +} + +function renderWhatsappSummaryCard() { + const card = $('whatsappAiSummaryCard'); + const content = $('whatsappAiSummaryContent'); + const meta = $('whatsappAiSummaryMeta'); + const status = $('whatsappAiSummaryStatus'); + if (!card || !content) { + return; + } + const thread = selectedWhatsappChat(); + if (!whatsappSelectedSummaryVisible(thread)) { + card.classList.add('hidden'); + content.innerHTML = ''; + if (meta) { + meta.textContent = ''; + } + if (status) { + status.innerHTML = ''; + } + return; + } + const summary = state.whatsapp.selectedThreadSummary; + const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(whatsappChatTimeLabel(summary.generated_at))}` : ''; + content.innerHTML = [ + whatsappSummaryFieldMarkup('Запрос клиента', summary.customer_request_text), + whatsappSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text), + whatsappSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason), + whatsappSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step), + ].join(''); + if (meta) { + meta.textContent = generatedAt; + } + if (status) { + status.innerHTML = whatsappSummaryStatusMarkup(summary); + } + card.classList.remove('hidden'); +} + +function telegramThreadSummary(thread) { + return thread.username ? `@${thread.username}` : `chat ${thread.chat_id}`; +} + +function telegramThreadStatusText(thread) { + return statusMeta(thread?.status || '').label; +} + +function telegramThreadAiStateMeta(thread) { + const stateKey = String(thread?.ai_state || '').trim(); + if (!stateKey) { + return null; + } + const meta = { + queued: { label: 'AI готов', tone: 'queued' }, + thinking: { label: 'AI думает', tone: 'thinking' }, + active: { label: 'AI активен', tone: 'active' }, + handoff_required: { label: 'Ждёт человека', tone: 'handoff' }, + human_owned: { label: 'Человек', tone: 'human' }, + closed: { label: 'AI завершён', tone: 'closed' }, + error: { label: 'Ошибка AI', tone: 'error' }, + }; + return meta[stateKey] || { label: stateKey, tone: 'queued' }; +} + +function telegramThreadAiClaimable(thread) { + return Boolean(thread && !thread.claimed_by_user && ['queued', 'thinking', 'active', 'handoff_required'].includes(String(thread.ai_state || ''))); +} + +function telegramClaimButtonLabel(thread) { + return telegramThreadAiClaimable(thread) ? 'Забрать у AI' : 'В работу'; +} + +function telegramThreadUnreadCount(thread) { + const raw = Number(thread?.unread_count ?? 0); + return Number.isFinite(raw) && raw > 0 ? raw : 0; +} + +function telegramThreadPresenceText(thread) { + if (!thread) { + return 'нет диалога'; + } + const aiMeta = telegramThreadAiStateMeta(thread); + if (aiMeta) { + return aiMeta.label; + } + return thread.claimed_by_user ? 'в работе' : 'свободен'; +} + +function telegramThreadOwnerText(thread) { + if (telegramThreadAiClaimable(thread)) { + return 'владелец AI'; + } + return thread?.claimed_by_user ? `владелец ${thread.claimed_by_user}` : 'владелец —'; +} + +function telegramThreadHeaderPresence(thread) { + if (!thread) { + return 'Выберите диалог слева'; + } + if (thread.status === 'closed') { + return 'был(а) недавно'; + } + const aiMeta = telegramThreadAiStateMeta(thread); + if (aiMeta && thread.ai_state !== 'human_owned') { + return aiMeta.label; + } + return thread.claimed_by_user ? 'в работе' : 'в сети'; +} + +function telegramThreadNeedsSummary(thread) { + if (!thread) { + return false; + } + return ['handoff_required', 'human_owned'].includes(String(thread.ai_state || '')); +} + +function telegramThreadSummaryAccessible(thread) { + if (!telegramThreadNeedsSummary(thread)) { + return false; + } + if (String(state.role || '') !== 'operator') { + return true; + } + const claimedByUser = String(thread?.claimed_by_user || '').trim(); + return !claimedByUser || claimedByUser === String(state.user || '').trim(); +} + +function telegramSelectedSummaryVisible(thread) { + if (!telegramThreadSummaryAccessible(thread)) { + return false; + } + const summary = state.telegram.selectedThreadSummary; + return Boolean(summary && summary.thread_id === thread?.thread_id); +} + +function telegramSummaryFieldMarkup(label, value) { + return ` +
            +
            ${escapeHtml(label)}
            +
            ${escapeHtml(value || '—')}
            +
            + `; +} + +function telegramSummaryStatusMarkup(summary) { + if (!summary?.status_label) { + return ''; + } + const tone = String(summary.status_tone || 'handoff').trim() || 'handoff'; + return `${escapeHtml(summary.status_label)}`; +} + +function renderTelegramSummaryCard() { + const card = $('telegramAiSummaryCard'); + const content = $('telegramAiSummaryContent'); + const meta = $('telegramAiSummaryMeta'); + const status = $('telegramAiSummaryStatus'); + if (!card || !content) { + return; + } + const thread = selectedTelegramThread(); + if (!telegramSelectedSummaryVisible(thread)) { + card.classList.add('hidden'); + content.innerHTML = ''; + if (meta) { + meta.textContent = ''; + } + if (status) { + status.innerHTML = ''; + } + return; + } + const summary = state.telegram.selectedThreadSummary; + const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(telegramThreadTimeLabel(summary.generated_at))}` : ''; + content.innerHTML = [ + telegramSummaryFieldMarkup('Запрос клиента', summary.customer_request_text), + telegramSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text), + telegramSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason), + telegramSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step), + ].join(''); + if (meta) { + meta.textContent = generatedAt; + } + if (status) { + status.innerHTML = telegramSummaryStatusMarkup(summary); + } + card.classList.remove('hidden'); +} + +function telegramThreadCountLabel(count) { + const mod10 = count % 10; + const mod100 = count % 100; + if (mod10 === 1 && mod100 !== 11) { + return `${count} диалог`; + } + if ([2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100)) { + return `${count} диалога`; + } + return `${count} диалогов`; +} + +function telegramVisibleThreads() { + const query = String(state.telegram.searchQuery || '').trim().toLowerCase(); + return state.telegram.threads.filter((thread) => { + if (!query) { + return true; + } + return [ + telegramThreadDisplayName(thread), + thread.username ? `@${thread.username}` : '', + thread.chat_id, + thread.last_message_preview, + ].some((value) => String(value || '').toLowerCase().includes(query)); + }); +} + +function resetTelegramMessageSearch() { + state.telegram.messageSearchQuery = ''; + state.telegram.messageSearchOpen = false; +} + +function telegramMessageSearchQuery() { + return String(state.telegram.messageSearchQuery || '').trim(); +} + +function telegramMessageMatchesSearch(message) { + const query = telegramMessageSearchQuery().toLowerCase(); + if (!query) { + return true; + } + return String(message?.text || '').toLowerCase().includes(query); +} + +function telegramVisibleMessages() { + return state.telegram.messages.filter((message) => telegramMessageMatchesSearch(message)); +} + +function telegramMessageSearchMeta(thread) { + if (!thread) { + return 'Выберите чат для поиска'; + } + const query = telegramMessageSearchQuery(); + if (!query) { + return 'Поиск по текущему чату'; + } + const matches = telegramVisibleMessages().length; + if (matches === 0) { + return 'Ничего не найдено'; + } + if (matches === 1) { + return '1 совпадение'; + } + return `${matches} совпадений`; +} + +function highlightTelegramMessageText(value) { + const source = String(value || 'Сообщение без текста.'); + const query = telegramMessageSearchQuery(); + if (!query) { + return escapeHtml(source); + } + const matcher = new RegExp(`(${escapeRegExp(query)})`, 'gi'); + return source + .split(matcher) + .map((part) => (part.toLowerCase() === query.toLowerCase() + ? `${escapeHtml(part)}` + : escapeHtml(part))) + .join(''); +} + +function toggleTelegramMessageSearch(force = null) { + const hasThread = Boolean(selectedTelegramThread()); + const nextOpen = force === null ? !state.telegram.messageSearchOpen : Boolean(force); + state.telegram.messageSearchOpen = hasThread && nextOpen; + if (!state.telegram.messageSearchOpen) { + state.telegram.messageSearchQuery = ''; + } + renderTelegramWorkspace(); + if (state.telegram.messageSearchOpen) { + window.requestAnimationFrame(() => { + $('telegramMessageSearch')?.focus(); + $('telegramMessageSearch')?.select(); + }); + } +} + +function telegramMessageDayKey(value) { + if (!value) { + return ''; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return ''; + } + return `${dt.getFullYear()}-${dt.getMonth()}-${dt.getDate()}`; +} + +function telegramMessageDayLabel(value) { + if (!value) { + return 'Недавно'; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return String(value); + } + const now = new Date(); + const sameDay = dt.toDateString() === now.toDateString(); + if (sameDay) { + return 'Сегодня'; + } + const yesterday = new Date(now); + yesterday.setDate(now.getDate() - 1); + if (dt.toDateString() === yesterday.toDateString()) { + return 'Вчера'; + } + return dt.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }); +} + +function telegramMessageStatusMarkup(message) { + if (message.direction !== 'outbound' || !message.delivery_status) { + return ''; + } + const status = String(message.delivery_status || '').toLowerCase(); + const checks = status.includes('deliver') || status.includes('read') || status.includes('sent') ? '✓✓' : '✓'; + return `${checks}`; +} + +function syncTelegramComposerHeight() { + const input = $('telegramReplyText'); + if (!input) { + return; + } + input.style.height = '0px'; + const nextHeight = Math.min(Math.max(input.scrollHeight, 54), 164); + input.style.height = `${nextHeight}px`; +} + +function renderTelegramThreadsList() { + const target = $('telegramThreadsList'); + const threads = telegramVisibleThreads(); + if (!threads.length) { + target.innerHTML = '
            Пока нет Telegram диалогов.
            '; + return; + } + target.innerHTML = threads + .map((thread) => { + const selected = thread.thread_id === state.telegram.selectedThreadId ? ' selected' : ''; + const preview = thread.last_message_preview || 'Нет сообщений.'; + const unread = telegramThreadUnreadCount(thread); + const aiMeta = telegramThreadAiStateMeta(thread); + return ` + + `; + }) + .join(''); +} + +function renderTelegramMessagesTimeline() { + const target = $('telegramMessagesTimeline'); + const thread = selectedTelegramThread(); + if (!thread) { + target.innerHTML = '
            Выберите диалог слева, чтобы увидеть историю сообщений.
            '; + return; + } + if (!state.telegram.messages.length) { + target.innerHTML = '
            Сообщений пока нет.
            '; + return; + } + const messages = telegramVisibleMessages(); + if (!messages.length) { + target.innerHTML = '
            Ничего не найдено в этом чате.
            '; + return; + } + const fragments = messages.map((message) => { + const direction = message.direction || 'system'; + if (direction === 'system') { + return ` +
            +
            ${highlightTelegramMessageText(message.text || 'Системное сообщение')}
            +
            + `; + } + const mine = direction === 'outbound'; + const aiBadge = message.author_type === 'ai' + ? 'AI' + : ''; + return ` +
            +
            + ${aiBadge} +
            ${highlightTelegramMessageText(message.text || 'Сообщение без текста.')}
            +
            + ${escapeHtml(telegramThreadTimeLabel(message.created_at))} + ${mine ? telegramMessageStatusMarkup(message) : ''} +
            +
            +
            + `; + }); + target.innerHTML = fragments.join(''); + target.scrollTop = telegramMessageSearchQuery() ? 0 : target.scrollHeight; +} + +function syncTelegramActionButtons() { + const thread = selectedTelegramThread(); + const pending = state.telegram.pendingAction; + const replyText = $('telegramReplyText').value.trim(); + $('telegramClaimThreadBtn').textContent = telegramClaimButtonLabel(thread); + $('telegramClaimThreadBtn').disabled = pending !== '' || !telegramThreadCanClaim(thread); + $('telegramReturnToAiBtn').disabled = pending !== '' || !telegramThreadCanReturnToAi(thread); + $('telegramCloseThreadBtn').disabled = pending !== '' || !telegramThreadCanManage(thread) || thread?.status === 'closed'; + $('telegramEscalateThreadBtn').disabled = pending !== '' || !telegramThreadCanManage(thread); + $('telegramSendReplyBtn').disabled = pending !== '' || !telegramThreadCanReply(thread) || !replyText; + syncTelegramComposerHeight(); +} + +function syncTelegramOperatorTray() { + $('telegramOperatorTray').classList.toggle('hidden', !state.telegram.operatorTrayOpen); +} + +function toggleTelegramOperatorTray(force = null) { + state.telegram.operatorTrayOpen = force === null ? !state.telegram.operatorTrayOpen : Boolean(force); + syncTelegramOperatorTray(); +} + +function renderTelegramWorkspace() { + renderTelegramThreadsList(); + renderTelegramSummaryCard(); + renderTelegramMessagesTimeline(); + const thread = selectedTelegramThread(); + const avatar = $('telegramThreadAvatar'); + const presence = $('telegramThreadPresenceChip'); + const owner = $('telegramThreadOwnerChip'); + const messageSearchButton = $('telegramHeaderSearchBtn'); + const messageSearchBar = $('telegramMessageSearchBar'); + const messageSearchInput = $('telegramMessageSearch'); + const messageSearchMeta = $('telegramMessageSearchMeta'); + const messageSearchClearBtn = $('telegramMessageSearchClearBtn'); + const aiChip = $('telegramThreadAiChip'); + const aiMeta = telegramThreadAiStateMeta(thread); + $('telegramThreadDisplayName').textContent = thread + ? telegramThreadDisplayName(thread) + : 'Выберите диалог'; + $('telegramThreadMeta').textContent = thread + ? telegramThreadHeaderPresence(thread) + : 'Выберите диалог слева.'; + if (avatar) { + avatar.textContent = thread ? telegramThreadInitials(thread) : 'TG'; + const [start, end] = telegramAvatarPalette(thread ? telegramThreadDisplayName(thread) : 'Telegram'); + avatar.style.setProperty('--telegram-avatar-start', start); + avatar.style.setProperty('--telegram-avatar-end', end); + } + if (presence) { + presence.textContent = thread ? telegramThreadPresenceText(thread) : 'нет диалога'; + const presenceTone = aiMeta + ? aiMeta.tone + : (thread?.claimed_by_user ? 'claimed' : 'free'); + presence.className = `telegram-operator-chip ${presenceTone}`; + } + if (owner) { + owner.textContent = thread ? telegramThreadOwnerText(thread) : 'владелец —'; + owner.className = 'telegram-operator-chip subtle'; + } + if (aiChip) { + if (aiMeta) { + aiChip.textContent = aiMeta.label; + aiChip.className = `telegram-thread-ai-chip ${aiMeta.tone}`; + } else { + aiChip.textContent = ''; + aiChip.className = 'telegram-thread-ai-chip hidden'; + } + } + if (messageSearchButton) { + messageSearchButton.disabled = !thread; + messageSearchButton.classList.toggle('active', Boolean(thread) && state.telegram.messageSearchOpen); + } + if (messageSearchBar) { + messageSearchBar.classList.toggle('hidden', !thread || !state.telegram.messageSearchOpen); + } + if (messageSearchInput) { + messageSearchInput.value = state.telegram.messageSearchQuery || ''; + messageSearchInput.disabled = !thread; + } + if (messageSearchMeta) { + messageSearchMeta.textContent = telegramMessageSearchMeta(thread); + } + if (messageSearchClearBtn) { + messageSearchClearBtn.textContent = telegramMessageSearchQuery() ? 'Очистить' : 'Закрыть'; + } + $('telegramComposerHint').textContent = thread + ? telegramThreadCanReply(thread) + ? 'Reply уйдёт через Telegram Bot API и сохранится в thread history.' + : telegramThreadCanReturnToAi(thread) + ? 'Диалог сейчас у человека. Нажмите «Вернуть AI», чтобы снова включить автоответ.' + : telegramThreadAiClaimable(thread) + ? 'Диалог сейчас у AI. Заберите его в работу, чтобы ответить вручную.' + : 'Reply доступен после claim или для admin/supervisor.' + : 'Выберите thread, чтобы отвечать в Telegram.'; + syncTelegramOperatorTray(); + syncTelegramComposerHeight(); + syncTelegramActionButtons(); +} + +function applyTelegramCollections(threads, options = {}) { + const { preserveSelection = true } = options; + const sorted = [...(threads || [])].sort((left, right) => { + return String(right.last_message_at || '').localeCompare(String(left.last_message_at || '')); + }); + state.telegram.threads = sorted; + const previous = preserveSelection ? state.telegram.selectedThreadId : ''; + const stillExists = sorted.some((thread) => thread.thread_id === previous); + state.telegram.selectedThreadId = stillExists ? previous : (sorted[0]?.thread_id || ''); + if (state.telegram.selectedThreadId !== previous) { + resetTelegramMessageSearch(); + state.telegram.selectedThreadSummary = null; + } + if (!state.telegram.selectedThreadId) { + state.telegram.selectedThreadSummary = null; + } + syncMessengerConversations({ preserveSelection: true }); + renderTelegramWorkspace(); + renderMessengerWorkspace(); + renderCustomerSpotlight(); + renderUnifiedInbox(); +} + +async function loadTelegramThreadMessages(threadId = state.telegram.selectedThreadId, logResult = false) { + if (!threadId) { + state.telegram.messages = []; + state.telegram.selectedThreadSummary = null; + resetTelegramMessageSearch(); + renderTelegramWorkspace(); + return []; + } + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/messages`); + if (threadId === state.telegram.selectedThreadId) { + state.telegram.messages = Array.isArray(data) ? data : []; + renderTelegramWorkspace(); + renderCustomerSpotlight(); + } + if (logResult) { + log('Сообщения Telegram-диалога обновлены', { thread_id: threadId, messages: Array.isArray(data) ? data.length : 0 }); + } + return Array.isArray(data) ? data : []; +} + +async function loadTelegramThreadSummary(threadId = state.telegram.selectedThreadId, options = {}) { + const { logResult = false } = options; + if (!threadId) { + state.telegram.selectedThreadSummary = null; + renderTelegramWorkspace(); + return null; + } + const thread = state.telegram.threads.find((item) => item.thread_id === threadId) || null; + if (!telegramThreadSummaryAccessible(thread)) { + if (threadId === state.telegram.selectedThreadId) { + state.telegram.selectedThreadSummary = null; + renderTelegramWorkspace(); + } + return null; + } + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/ai-summary`); + if (threadId === state.telegram.selectedThreadId) { + state.telegram.selectedThreadSummary = data || null; + renderTelegramWorkspace(); + } + if (logResult && data) { + log('AI-сводка загружена', { thread_id: threadId, session_id: data.session_id }); + } + return data || null; + } catch (err) { + if (threadId === state.telegram.selectedThreadId) { + state.telegram.selectedThreadSummary = null; + renderTelegramWorkspace(); + } + if (logResult) { + log('Не удалось загрузить AI-сводку', { thread_id: threadId, error: err.message }); + } + return null; + } +} + +async function loadTelegramThreads(logResult = true, options = {}) { + const { preserveSelection = true, preserveOnError = false } = options; + try { + const data = await api('telegram', 'integrations/telegram/threads'); + applyTelegramCollections(Array.isArray(data) ? data : [], { preserveSelection }); + await loadTelegramThreadMessages(state.telegram.selectedThreadId, false); + await loadTelegramThreadSummary(state.telegram.selectedThreadId, { logResult: false }); + if (logResult) { + log('Telegram-диалоги обновлены', { threads: state.telegram.threads.length }); + } + } catch (err) { + if (!preserveOnError) { + state.telegram.threads = []; + state.telegram.selectedThreadId = ''; + state.telegram.messages = []; + state.telegram.selectedThreadSummary = null; + renderTelegramWorkspace(); + } + if (logResult) { + log('Не удалось загрузить Telegram-диалоги', { error: err.message }); + } + } +} + +async function selectTelegramThread(threadId) { + if ((threadId || '') !== state.telegram.selectedThreadId) { + resetTelegramMessageSearch(); + } + state.telegram.selectedThreadId = threadId || ''; + state.telegram.selectedThreadSummary = null; + state.telegram.operatorTrayOpen = false; + renderTelegramWorkspace(); + await loadTelegramThreadMessages(threadId, false); + await loadTelegramThreadSummary(threadId, { logResult: false }); +} + +async function claimTelegramThread() { + const thread = selectedTelegramThread(); + if (!thread) { + return; + } + state.telegram.pendingAction = 'claim'; + renderTelegramWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/claim`, { + method: 'POST', + }); + const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item)); + applyTelegramCollections(nextThreads); + await loadTelegramThreadSummary(data.thread_id, { logResult: false }); + refreshLiveCallsInBackground(); + loadInteractions().catch(() => {}); + log('Telegram-диалог взят в работу', { thread_id: data.thread_id, interaction_id: data.interaction_id }); + } catch (err) { + log('Не удалось взять Telegram-диалог в работу', { error: err.message }); + } finally { + state.telegram.pendingAction = ''; + renderTelegramWorkspace(); + } +} + +async function returnTelegramThreadToAi() { + const thread = selectedTelegramThread(); + if (!thread) { + return; + } + state.telegram.pendingAction = 'return-ai'; + renderTelegramWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/return-to-ai`, { + method: 'POST', + }); + const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item)); + applyTelegramCollections(nextThreads); + await loadTelegramThreadSummary(data.thread_id, { logResult: false }); + loadInteractions().catch(() => {}); + log('Telegram-диалог возвращён AI', { thread_id: data.thread_id, interaction_id: data.interaction_id }); + } catch (err) { + log('Не удалось вернуть Telegram-диалог AI', { error: err.message }); + } finally { + state.telegram.pendingAction = ''; + renderTelegramWorkspace(); + } +} + +async function sendTelegramReply() { + const thread = selectedTelegramThread(); + const text = $('telegramReplyText').value.trim(); + if (!thread || !text) { + return; + } + state.telegram.pendingAction = 'reply'; + renderTelegramWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/messages`, { + method: 'POST', + body: JSON.stringify({ text }), + }); + $('telegramReplyText').value = ''; + state.telegram.messages = [...state.telegram.messages, data]; + const nextThreads = state.telegram.threads.map((item) => { + if (item.thread_id !== thread.thread_id) { + return item; + } + return { + ...item, + last_message_at: data.created_at, + last_message_preview: data.text, + }; + }); + applyTelegramCollections(nextThreads); + renderTelegramWorkspace(); + log('Ответ в Telegram поставлен в очередь', { thread_id: thread.thread_id, delivery_status: data.delivery_status || 'pending' }); + window.setTimeout(() => loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }), 0); + } catch (err) { + log('Не удалось отправить ответ в Telegram', { error: err.message }); + } finally { + state.telegram.pendingAction = ''; + renderTelegramWorkspace(); + } +} + +async function closeTelegramThread() { + const thread = selectedTelegramThread(); + if (!thread) { + return; + } + state.telegram.pendingAction = 'close'; + renderTelegramWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/close`, { + method: 'POST', + }); + const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item)); + applyTelegramCollections(nextThreads); + await loadTelegramThreadSummary(data.thread_id, { logResult: false }); + loadInteractions().catch(() => {}); + log('Telegram-диалог закрыт', { thread_id: data.thread_id }); + } catch (err) { + log('Не удалось закрыть Telegram-диалог', { error: err.message }); + } finally { + state.telegram.pendingAction = ''; + renderTelegramWorkspace(); + } +} + +async function escalateTelegramThread() { + const thread = selectedTelegramThread(); + const targetQueueId = $('telegramEscalationQueue').value.trim() || DEMO_QUEUE; + if (!thread) { + return; + } + state.telegram.pendingAction = 'escalate'; + renderTelegramWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(thread.thread_id)}/escalate`, { + method: 'POST', + body: JSON.stringify({ target_queue_id: targetQueueId }), + }); + const nextThreads = state.telegram.threads.map((item) => (item.thread_id === data.thread_id ? data : item)); + applyTelegramCollections(nextThreads); + await loadTelegramThreadSummary(data.thread_id, { logResult: false }); + loadInteractions().catch(() => {}); + log('Telegram-диалог эскалирован', { thread_id: data.thread_id, queue_id: data.queue_id }); + } catch (err) { + log('Не удалось эскалировать Telegram-диалог', { error: err.message }); + } finally { + state.telegram.pendingAction = ''; + renderTelegramWorkspace(); + } +} + +function startTelegramPolling() { + if (state.telegram.pollTimer) { + window.clearInterval(state.telegram.pollTimer); + state.telegram.pollTimer = null; + } + state.telegram.pollTimer = window.setInterval(() => { + if (operatorHashState().view === 'messages') { + return; + } + loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }); + }, 5000); +} + +function messengerConversationId(channel, threadId) { + return `${channel}:${threadId || ''}`; +} + +function messengerConversationFromTelegramThread(thread) { + const customerId = thread?.customer_id || customerIdForInteractionId(thread?.interaction_id) || ''; + const unread = telegramThreadUnreadCount(thread); + return { + conversation_id: messengerConversationId('telegram', thread?.thread_id), + channel: 'telegram', + channel_label: 'Telegram', + thread_id: thread?.thread_id || '', + customer_id: customerId, + interaction_id: thread?.interaction_id || '', + display_name: telegramThreadDisplayName(thread), + last_message_preview: thread?.last_message_preview || '', + last_message_at: thread?.last_message_at || thread?.updated_at || thread?.created_at || '', + status: thread?.status || '', + claimed_by_user: thread?.claimed_by_user || '', + ai_state: thread?.ai_state || '', + unread, + attention: unread > 0 || ['handoff_required', 'handoff_requested'].includes(String(thread?.ai_state || '')), + source_thread: thread, + }; +} + +function buildMessengerConversations() { + return state.telegram.threads + .map((thread) => messengerConversationFromTelegramThread(thread)) + .sort((left, right) => { + return String(right.last_message_at || '').localeCompare(String(left.last_message_at || '')); + }); +} + +function syncMessengerConversations(options = {}) { + const { preserveSelection = true } = options; + const previous = preserveSelection ? state.messenger.selectedConversationId : ''; + const conversations = buildMessengerConversations(); + const stillExists = conversations.some((item) => item.conversation_id === previous); + state.messenger.conversations = conversations; + if (!stillExists) { + state.messenger.selectedConversationId = ''; + state.messenger.messages = []; + state.messenger.selectedSummary = null; + } +} + +function selectedMessengerConversation() { + return state.messenger.conversations.find((item) => item.conversation_id === state.messenger.selectedConversationId) || null; +} + +function selectedMessengerThread() { + const conversation = selectedMessengerConversation(); + if (!conversation || conversation.channel !== 'telegram') { + return null; + } + return state.telegram.threads.find((thread) => thread.thread_id === conversation.thread_id) || conversation.source_thread || null; +} + +function messengerConversationMatchesFilter(conversation) { + const filter = state.messenger.activeFilter || 'all'; + if (filter === 'new') { + return conversation.status === 'new' || conversation.unread > 0 || !conversation.claimed_by_user; + } + if (filter === 'mine') { + return String(conversation.claimed_by_user || '') === String(state.user || ''); + } + if (filter === 'ai') { + return ['handoff_required', 'handoff_requested', 'human_owned'].includes(String(conversation.ai_state || '')); + } + return true; +} + +function messengerVisibleConversations() { + return state.messenger.conversations.filter((conversation) => messengerConversationMatchesFilter(conversation)); +} + +function messengerConversationCanClaim(conversation) { + return Boolean(conversation && conversation.status !== 'closed' && !conversation.claimed_by_user); +} + +function messengerConversationCanReply(conversation) { + return Boolean(conversation && conversation.status !== 'closed' && String(conversation.claimed_by_user || '') === String(state.user || '')); +} + +function messengerConversationCanManage(conversation) { + if (!conversation || conversation.status === 'closed') { + return false; + } + if (state.role === 'admin' || state.role === 'supervisor') { + return true; + } + return String(conversation.claimed_by_user || '') === String(state.user || ''); +} + +function messengerAiStateMeta(conversation) { + return telegramThreadAiStateMeta(conversation?.source_thread || selectedMessengerThread()); +} + +function messengerContextCustomerId(conversation = selectedMessengerConversation()) { + return conversation?.customer_id || customerIdForInteractionId(conversation?.interaction_id) || ''; +} + +function messengerCustomerProfileAvailable(conversation = selectedMessengerConversation()) { + const customerId = messengerContextCustomerId(conversation); + return Boolean(customerId && state.customers.items.some((item) => item.customer_id === customerId)); +} + +function messengerSummaryVisible(conversation) { + if (!conversation || conversation.channel !== 'telegram') { + return false; + } + const summary = state.messenger.selectedSummary; + return Boolean(summary && summary.thread_id === conversation.thread_id); +} + +function renderMessengerConversations() { + const target = $('messengerConversationList'); + if (!target) { + return; + } + if (state.messenger.backendError && !state.messenger.conversations.length) { + target.innerHTML = ` +
            + Backend недоступен + ${escapeHtml(state.messenger.backendError)} +
            + `; + return; + } + const conversations = messengerVisibleConversations(); + if (!state.messenger.conversations.length) { + target.innerHTML = '
            Новых сообщений пока нет
            '; + return; + } + if (!conversations.length) { + target.innerHTML = `
            Нет диалогов в фильтре «${escapeHtml(MESSENGER_FILTERS[state.messenger.activeFilter] || 'Все')}»
            `; + return; + } + target.innerHTML = conversations + .map((conversation) => { + const selected = conversation.conversation_id === state.messenger.selectedConversationId ? ' selected' : ''; + const preview = conversation.last_message_preview || 'Нет сообщений.'; + const aiMeta = messengerAiStateMeta(conversation); + return ` + + `; + }) + .join(''); +} + +function renderMessengerMessages() { + const target = $('messengerMessagesTimeline'); + const conversation = selectedMessengerConversation(); + if (!target) { + return; + } + if (!conversation) { + target.innerHTML = '
            Выберите диалог слева
            '; + return; + } + if (!state.messenger.messages.length) { + target.innerHTML = '
            Сообщений пока нет.
            '; + return; + } + target.innerHTML = state.messenger.messages + .map((message) => { + const direction = message.direction || 'system'; + if (direction === 'system') { + return ` +
            +
            ${escapeHtml(message.text || 'Системное сообщение')}
            +
            + `; + } + const mine = direction === 'outbound'; + const aiBadge = message.author_type === 'ai' + ? 'AI' + : ''; + return ` +
            +
            + ${aiBadge} +
            ${escapeHtml(message.text || 'Сообщение без текста.')}
            +
            + ${escapeHtml(telegramThreadTimeLabel(message.created_at))} + ${mine ? telegramMessageStatusMarkup(message) : ''} +
            +
            +
            + `; + }) + .join(''); + target.scrollTop = target.scrollHeight; +} + +function renderMessengerSummaryCard() { + const card = $('messengerAiSummaryCard'); + const content = $('messengerAiSummaryContent'); + const meta = $('messengerAiSummaryMeta'); + const status = $('messengerAiSummaryStatus'); + const conversation = selectedMessengerConversation(); + if (!card || !content) { + return; + } + if (!messengerSummaryVisible(conversation)) { + card.classList.add('hidden'); + content.innerHTML = ''; + if (meta) { + meta.textContent = ''; + } + if (status) { + status.innerHTML = ''; + } + return; + } + const summary = state.messenger.selectedSummary; + const generatedAt = summary?.generated_at ? `Сформировано ${escapeHtml(telegramThreadTimeLabel(summary.generated_at))}` : ''; + content.innerHTML = [ + telegramSummaryFieldMarkup('Запрос клиента', summary.customer_request_text), + telegramSummaryFieldMarkup('Что сделал AI', summary.ai_outcome_text), + telegramSummaryFieldMarkup('Почему передал человеку', summary.handoff_reason), + telegramSummaryFieldMarkup('Что делать дальше', summary.recommended_next_step), + ].join(''); + if (meta) { + meta.textContent = generatedAt; + } + if (status) { + status.innerHTML = telegramSummaryStatusMarkup(summary); + } + card.classList.remove('hidden'); +} + +function renderMessengerContext() { + const conversation = selectedMessengerConversation(); + const customerId = messengerContextCustomerId(conversation); + const interactionId = conversation?.interaction_id || ''; + const aiMeta = messengerAiStateMeta(conversation); + const profileAvailable = messengerCustomerProfileAvailable(conversation); + const preview = conversation?.last_message_preview || ''; + const summary = messengerSummaryVisible(conversation) ? state.messenger.selectedSummary : null; + const summaryText = summary + ? [summary.customer_request_text, summary.recommended_next_step].filter(Boolean).join(' · ') + : ''; + if ($('messengerContextName')) { + $('messengerContextName').textContent = conversation ? conversation.display_name : 'Диалог не выбран'; + } + if ($('messengerContextChannel')) { + $('messengerContextChannel').textContent = conversation?.channel_label || 'Telegram'; + } + if ($('messengerContextStatus')) { + $('messengerContextStatus').textContent = conversation + ? [statusMeta(conversation.status).label, aiMeta?.label].filter(Boolean).join(' · ') + : '—'; + } + if ($('messengerContextCustomer')) { + $('messengerContextCustomer').textContent = customerId ? customerDisplayName(customerId) : '—'; + } + if ($('messengerContextInteraction')) { + $('messengerContextInteraction').textContent = interactionId || '—'; + } + if ($('messengerContextPreview')) { + $('messengerContextPreview').textContent = preview || (conversation ? 'Нет сообщений.' : 'Выберите диалог слева.'); + } + if ($('messengerContextAiSummary')) { + $('messengerContextAiSummary').textContent = summaryText || (conversation ? 'AI summary пока недоступен.' : 'Сводка появится после AI handoff.'); + } + ['messengerOpenProfileBtn', 'messengerContextOpenProfileBtn'].forEach((id) => { + const button = $(id); + if (button) { + button.disabled = !profileAvailable; + } + }); +} + +function syncMessengerActionButtons() { + const conversation = selectedMessengerConversation(); + const pending = state.messenger.pendingAction; + const replyText = String($('messengerReplyText')?.value || '').trim(); + const canClaim = messengerConversationCanClaim(conversation); + const canReply = messengerConversationCanReply(conversation); + const canManage = messengerConversationCanManage(conversation); + const claimButton = $('messengerClaimBtn'); + const closeButton = $('messengerCloseBtn'); + const sendButton = $('messengerSendReplyBtn'); + const composer = $('messengerReplyText'); + if (claimButton) { + claimButton.disabled = pending !== '' || !canClaim; + claimButton.textContent = telegramThreadAiClaimable(selectedMessengerThread()) ? 'Забрать у AI' : 'Взять в работу'; + } + if (closeButton) { + closeButton.disabled = pending !== '' || !canManage; + } + if (sendButton) { + sendButton.disabled = pending !== '' || !canReply || !replyText; + } + if (composer) { + composer.disabled = pending !== '' || !canReply; + } + if ($('messengerComposerHint')) { + $('messengerComposerHint').textContent = conversation + ? canReply + ? 'Ответ уйдёт через Telegram endpoint выбранного диалога.' + : canClaim + ? 'Сначала нажмите «Взять в работу», после этого будет доступен ответ.' + : 'Ответ недоступен: диалог закрыт или закреплён за другим оператором.' + : 'Выберите диалог слева.'; + } +} + +function syncMessengerComposerHeight() { + const input = $('messengerReplyText'); + if (!input) { + return; + } + input.style.height = '0px'; + const nextHeight = Math.min(Math.max(input.scrollHeight, 54), 148); + input.style.height = `${nextHeight}px`; +} + +function renderMessengerWorkspace() { + if (!$('messengerWorkspace')) { + return; + } + const conversation = selectedMessengerConversation(); + const aiMeta = messengerAiStateMeta(conversation); + document.querySelectorAll('[data-messenger-filter]').forEach((button) => { + button.classList.toggle('active', button.dataset.messengerFilter === state.messenger.activeFilter); + }); + renderMessengerConversations(); + renderMessengerSummaryCard(); + renderMessengerMessages(); + renderMessengerContext(); + if ($('messengerConversationAvatar')) { + $('messengerConversationAvatar').textContent = conversation ? customerInitials(conversation.display_name) : 'TG'; + const [start, end] = telegramAvatarPalette(conversation ? conversation.display_name : 'Telegram'); + $('messengerConversationAvatar').style.setProperty('--telegram-avatar-start', start); + $('messengerConversationAvatar').style.setProperty('--telegram-avatar-end', end); + } + if ($('messengerConversationTitle')) { + $('messengerConversationTitle').textContent = conversation ? conversation.display_name : 'Выберите диалог'; + } + if ($('messengerConversationMeta')) { + $('messengerConversationMeta').textContent = conversation + ? [conversation.channel_label, statusMeta(conversation.status).label, aiMeta?.label, conversation.claimed_by_user ? `оператор ${conversation.claimed_by_user}` : 'не закреплён'] + .filter(Boolean) + .join(' · ') + : 'Выберите диалог слева.'; + } + if ($('messengerChannelBadge')) { + $('messengerChannelBadge').textContent = conversation?.channel_label || 'Telegram'; + } + if ($('messengerBackendError')) { + $('messengerBackendError').classList.toggle('hidden', !state.messenger.backendError); + const errorText = $('messengerBackendError').querySelector('span'); + if (errorText) { + errorText.textContent = state.messenger.backendError || 'Backend сообщений недоступен.'; + } + } + syncMessengerComposerHeight(); + syncMessengerActionButtons(); +} + +function setMessengerFilter(filter) { + state.messenger.activeFilter = MESSENGER_FILTERS[filter] ? filter : 'all'; + renderMessengerWorkspace(); +} + +async function loadMessengerMessages(threadId) { + if (!threadId) { + state.messenger.messages = []; + renderMessengerWorkspace(); + return []; + } + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/messages`); + const messages = Array.isArray(data) ? data : []; + const conversation = selectedMessengerConversation(); + if (conversation?.thread_id === threadId) { + state.messenger.messages = messages; + if (state.telegram.selectedThreadId === threadId) { + state.telegram.messages = messages; + } + renderMessengerWorkspace(); + } + return messages; +} + +async function loadMessengerSummary(threadId) { + if (!threadId) { + state.messenger.selectedSummary = null; + renderMessengerWorkspace(); + return null; + } + const thread = state.telegram.threads.find((item) => item.thread_id === threadId) || null; + if (!telegramThreadSummaryAccessible(thread)) { + state.messenger.selectedSummary = null; + renderMessengerWorkspace(); + return null; + } + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(threadId)}/ai-summary`); + const conversation = selectedMessengerConversation(); + if (conversation?.thread_id === threadId) { + state.messenger.selectedSummary = data || null; + renderMessengerWorkspace(); + } + return data || null; + } catch { + const conversation = selectedMessengerConversation(); + if (conversation?.thread_id === threadId) { + state.messenger.selectedSummary = null; + renderMessengerWorkspace(); + } + return null; + } +} + +async function loadMessengerThreads(logResult = false, options = {}) { + const { preserveSelection = true, preserveOnError = false } = options; + try { + const data = await api('telegram', 'integrations/telegram/threads'); + applyTelegramCollections(Array.isArray(data) ? data : [], { preserveSelection: true }); + syncMessengerConversations({ preserveSelection }); + state.messenger.backendError = ''; + renderMessengerWorkspace(); + const conversation = selectedMessengerConversation(); + if (conversation?.thread_id) { + await Promise.all([ + loadMessengerMessages(conversation.thread_id), + loadMessengerSummary(conversation.thread_id), + ]); + } + if (logResult) { + log('Сообщения обновлены', { conversations: state.messenger.conversations.length }); + } + } catch (err) { + state.messenger.backendError = err.message || 'Backend сообщений недоступен.'; + if (!preserveOnError) { + state.messenger.conversations = []; + state.messenger.selectedConversationId = ''; + state.messenger.messages = []; + state.messenger.selectedSummary = null; + } + renderMessengerWorkspace(); + if (logResult) { + log('Не удалось загрузить сообщения', { error: err.message }); + } + throw err; + } +} + +async function selectMessengerConversation(conversationId) { + state.messenger.selectedConversationId = conversationId || ''; + state.messenger.messages = []; + state.messenger.selectedSummary = null; + state.messenger.backendError = ''; + const conversation = selectedMessengerConversation(); + if (conversation?.channel === 'telegram') { + state.telegram.selectedThreadId = conversation.thread_id; + } + const customerId = messengerContextCustomerId(conversation); + if (customerId && state.customers.items.some((item) => item.customer_id === customerId)) { + selectCustomer(customerId); + ensureCustomerHistoryLoaded(customerId, { force: false }).catch(() => {}); + } + renderMessengerWorkspace(); + if (conversation?.thread_id) { + await Promise.all([ + loadMessengerMessages(conversation.thread_id), + loadMessengerSummary(conversation.thread_id), + ]); + } +} + +function upsertMessengerTelegramThread(thread, options = {}) { + const exists = state.telegram.threads.some((item) => item.thread_id === thread.thread_id); + const nextThreads = exists + ? state.telegram.threads.map((item) => (item.thread_id === thread.thread_id ? thread : item)) + : [thread, ...state.telegram.threads]; + applyTelegramCollections(nextThreads, { preserveSelection: true }); + syncMessengerConversations({ preserveSelection: true, ...options }); +} + +async function claimMessengerConversation() { + const conversation = selectedMessengerConversation(); + if (!conversation?.thread_id || !messengerConversationCanClaim(conversation)) { + return; + } + state.messenger.pendingAction = 'claim'; + renderMessengerWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/claim`, { + method: 'POST', + }); + upsertMessengerTelegramThread(data); + state.messenger.selectedConversationId = messengerConversationId('telegram', data.thread_id); + await loadMessengerSummary(data.thread_id); + loadInteractions().catch(() => {}); + log('Диалог взят в работу из Сообщений', { thread_id: data.thread_id, interaction_id: data.interaction_id }); + } catch (err) { + log('Не удалось взять диалог в работу', { error: err.message }); + } finally { + state.messenger.pendingAction = ''; + renderMessengerWorkspace(); + } +} + +async function sendMessengerReply() { + const conversation = selectedMessengerConversation(); + const text = String($('messengerReplyText')?.value || '').trim(); + if (!conversation?.thread_id || !text || !messengerConversationCanReply(conversation)) { + syncMessengerActionButtons(); + return; + } + state.messenger.pendingAction = 'reply'; + renderMessengerWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/messages`, { + method: 'POST', + body: JSON.stringify({ text }), + }); + $('messengerReplyText').value = ''; + state.messenger.composerText = ''; + state.messenger.messages = [...state.messenger.messages, data]; + if (state.telegram.selectedThreadId === conversation.thread_id) { + state.telegram.messages = [...state.telegram.messages, data]; + } + const nextThreads = state.telegram.threads.map((item) => { + if (item.thread_id !== conversation.thread_id) { + return item; + } + return { + ...item, + last_message_at: data.created_at, + last_message_preview: data.text, + }; + }); + applyTelegramCollections(nextThreads, { preserveSelection: true }); + renderMessengerWorkspace(); + log('Ответ из Сообщений отправлен через Telegram', { thread_id: conversation.thread_id, delivery_status: data.delivery_status || 'pending' }); + window.setTimeout(() => loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch(() => {}), 0); + } catch (err) { + log('Не удалось отправить ответ из Сообщений', { error: err.message }); + } finally { + state.messenger.pendingAction = ''; + renderMessengerWorkspace(); + } +} + +async function closeMessengerConversation() { + const conversation = selectedMessengerConversation(); + if (!conversation?.thread_id || !messengerConversationCanManage(conversation)) { + return; + } + state.messenger.pendingAction = 'close'; + renderMessengerWorkspace(); + try { + const data = await api('telegram', `integrations/telegram/threads/${encodeURIComponent(conversation.thread_id)}/close`, { + method: 'POST', + }); + upsertMessengerTelegramThread(data); + await loadMessengerSummary(data.thread_id); + loadInteractions().catch(() => {}); + log('Диалог закрыт из Сообщений', { thread_id: data.thread_id }); + } catch (err) { + log('Не удалось закрыть диалог из Сообщений', { error: err.message }); + } finally { + state.messenger.pendingAction = ''; + renderMessengerWorkspace(); + } +} + +function openMessengerCustomerProfile() { + const customerId = messengerContextCustomerId(); + if (!customerId || !messengerCustomerProfileAvailable()) { + return; + } + openCustomerProfile(customerId); +} + +function stopMessengerPolling() { + if (state.messenger.pollTimer) { + window.clearInterval(state.messenger.pollTimer); + state.messenger.pollTimer = null; + } +} + +function startMessengerPolling() { + stopMessengerPolling(); + state.messenger.pollTimer = window.setInterval(() => { + if (operatorHashState().view !== 'messages') { + return; + } + loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch(() => {}); + }, 5000); +} + +function selectedWhatsappChat() { + return state.whatsapp.chats.find((chat) => chat.id === state.whatsapp.selectedChatId) || null; +} + +function ensureWhatsappSelection() { + if (!state.whatsapp.chats.length) { + state.whatsapp.selectedChatId = ''; + return; + } + const exists = state.whatsapp.chats.some((chat) => chat.id === state.whatsapp.selectedChatId); + if (!exists) { + state.whatsapp.selectedChatId = state.whatsapp.chats[0].id; + } +} + +function whatsappChatInitials(chat) { + const parts = String(chat?.title || 'WA') + .trim() + .split(/\s+/) + .filter(Boolean) + .slice(0, 2); + if (!parts.length) { + return 'WA'; + } + return parts.map((part) => part[0].toUpperCase()).join(''); +} + +function whatsappChatTimeLabel(value) { + if (!value) { + return ''; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return ''; + } + const now = new Date(); + const sameDay = dt.toDateString() === now.toDateString(); + if (sameDay) { + return dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + return dt.toLocaleDateString([], { day: '2-digit', month: 'short' }); +} + +function whatsappMessageTimeLabel(value) { + if (!value) { + return ''; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return ''; + } + return dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +function whatsappMessageDayKey(value) { + if (!value) { + return ''; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return ''; + } + return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`; +} + +function whatsappMessageDayLabel(value) { + if (!value) { + return ''; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return ''; + } + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(today.getDate() - 1); + if (dt.toDateString() === today.toDateString()) { + return 'Today'; + } + if (dt.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } + return dt.toLocaleDateString([], { day: 'numeric', month: 'long' }); +} + +function whatsappVisibleChats() { + const query = String(state.whatsapp.searchQuery || '').trim().toLowerCase(); + return state.whatsapp.chats.filter((chat) => { + if (state.whatsapp.activeFilter === 'unread' && !(whatsappThreadUnreadCount(chat) > 0)) { + return false; + } + if (state.whatsapp.activeFilter === 'groups' && !chat.isGroup) { + return false; + } + if (!query) { + return true; + } + return [ + chat.title, + chat.handle, + chat.statusLine, + chat.lastMessagePreview, + ...(chat.messages || []).map((message) => message.text || ''), + ] + .join(' ') + .toLowerCase() + .includes(query); + }); +} + +function whatsappMessageStatusMarkup(message) { + if (message.direction !== 'out') { + return ''; + } + if (message.deliveryStatus === 'read') { + return ''; + } + if (message.deliveryStatus === 'delivered') { + return ''; + } + return ''; +} + +function whatsappComposerBlockedReason(chat) { + if (!chat) { + return 'Выберите чат'; + } + if (chat.status === 'closed') { + return 'Thread is closed'; + } + if (state.role === 'operator' && chat.claimedByUser && chat.claimedByUser !== state.user) { + return `Claimed by ${chat.claimedByUser}`; + } + return ''; +} + +function syncWhatsappComposerHeight() { + const input = $('whatsappComposerInput'); + if (!input) { + return; + } + input.style.height = 'auto'; + input.style.height = `${Math.min(Math.max(input.scrollHeight, 24), 120)}px`; +} + +function syncWhatsappComposerUi() { + const input = $('whatsappComposerInput'); + const sendBtn = $('whatsappSendBtn'); + if (!input || !sendBtn) { + return; + } + sendBtn.disabled = Boolean(whatsappComposerBlockedReason(selectedWhatsappChat())) + || !String(state.whatsapp.composerText || '').trim() + || Boolean(state.whatsapp.pendingAction); + syncWhatsappComposerHeight(); +} + +function renderWhatsappChatList() { + const target = $('whatsappChatList'); + if (!target) { + return; + } + const chats = whatsappVisibleChats(); + if (!chats.length) { + target.innerHTML = ` +
            + Чаты не найдены +

            Попробуйте другой запрос или переключите активный фильтр.

            +
            + `; + return; + } + target.innerHTML = chats.map((chat) => { + const [start, end] = whatsappAvatarPalette(chat.title); + const selected = chat.id === state.whatsapp.selectedChatId; + const aiMeta = whatsappThreadAiStateMeta(chat); + return ` + + `; + }).join(''); +} + +function renderWhatsappContextPanel() { + const panel = $('whatsappContextPanel'); + const aiChip = $('whatsappAiStateChip'); + const ownerChip = $('whatsappOwnerChip'); + const claimBtn = $('whatsappClaimBtn'); + const returnBtn = $('whatsappReturnToAiBtn'); + const chat = selectedWhatsappChat(); + if (!panel || !aiChip || !ownerChip || !claimBtn || !returnBtn) { + return; + } + const aiMeta = whatsappThreadAiStateMeta(chat); + const ownerText = whatsappThreadOwnerText(chat); + const showPanel = Boolean(chat && (aiMeta || chat.claimedByUser || whatsappSelectedSummaryVisible(chat))); + panel.classList.toggle('hidden', !showPanel); + + if (aiMeta) { + aiChip.textContent = aiMeta.label; + aiChip.className = `whatsapp-state-chip ${aiMeta.tone}`; + } else { + aiChip.textContent = ''; + aiChip.className = 'whatsapp-state-chip hidden'; + } + + if (chat) { + ownerChip.textContent = ownerText; + ownerChip.className = 'whatsapp-state-chip subtle'; + } else { + ownerChip.textContent = ''; + ownerChip.className = 'whatsapp-state-chip subtle hidden'; + } + + claimBtn.disabled = Boolean(state.whatsapp.pendingAction) || !whatsappThreadCanClaim(chat); + claimBtn.classList.toggle('hidden', !whatsappThreadCanClaim(chat)); + returnBtn.disabled = Boolean(state.whatsapp.pendingAction) || !whatsappThreadCanReturnToAi(chat); + returnBtn.classList.toggle('hidden', !whatsappThreadCanReturnToAi(chat)); + + renderWhatsappSummaryCard(); +} + +function renderWhatsappMessagesTimeline() { + const target = $('whatsappMessageTimeline'); + if (!target) { + return; + } + const chat = selectedWhatsappChat(); + if (!chat) { + target.innerHTML = ` +
            + Выберите чат +

            ${state.whatsapp.mode === 'live' ? 'Здесь появятся живые WhatsApp-диалоги.' : 'Демо-режим остаётся доступным, пока backend недоступен.'}

            +
            + `; + return; + } + if (!chat.messages?.length) { + target.innerHTML = ` +
            + Пока нет сообщений +

            ${state.whatsapp.mode === 'live' ? 'Ждём входящую активность WhatsApp в этом чате.' : 'Демо-рабочее место готово к первому сообщению.'}

            +
            + `; + return; + } + const fragments = [ + '
            Сообщения и звонки защищены сквозным шифрованием. Никто вне этого чата не может их прочитать или прослушать.
            ', + ]; + let lastDayKey = ''; + (chat.messages || []).forEach((message) => { + const dayKey = whatsappMessageDayKey(message.createdAt); + if (message.direction === 'system') { + fragments.push(`
            ${escapeHtml(message.text || whatsappMessageDayLabel(message.createdAt))}
            `); + lastDayKey = dayKey || lastDayKey; + return; + } + if (dayKey && dayKey !== lastDayKey) { + lastDayKey = dayKey; + fragments.push(`
            ${escapeHtml(whatsappMessageDayLabel(message.createdAt))}
            `); + } + const mine = message.direction === 'out'; + const aiBadge = mine && message.authorType === 'ai' + ? 'AI' + : ''; + const author = !mine && chat.isGroup && message.author + ? `
            ${escapeHtml(message.author)}
            ` + : ''; + fragments.push(` +
            +
            + ${aiBadge} + ${author} +
            ${escapeHtml(message.text || '')}
            +
            + ${escapeHtml(whatsappMessageTimeLabel(message.createdAt))} + ${whatsappMessageStatusMarkup(message)} +
            +
            +
            + `); + }); + target.innerHTML = fragments.join(''); + target.scrollTop = target.scrollHeight; +} + +function renderWhatsappWorkspace() { + if (!featureEnabled('whatsapp')) { + return; + } + ensureWhatsappSelection(); + renderWhatsappChatList(); + renderWhatsappContextPanel(); + renderWhatsappMessagesTimeline(); + const chat = selectedWhatsappChat(); + const searchInput = $('whatsappSearchInput'); + const composerInput = $('whatsappComposerInput'); + const avatar = $('whatsappChatAvatar'); + const paneCaption = $('whatsappPaneCaption'); + const blockedReason = whatsappComposerBlockedReason(chat); + const [start, end] = whatsappAvatarPalette(chat?.title || 'WhatsApp'); + if (searchInput && searchInput.value !== state.whatsapp.searchQuery) { + searchInput.value = state.whatsapp.searchQuery; + } + if (composerInput && composerInput.value !== state.whatsapp.composerText) { + composerInput.value = state.whatsapp.composerText; + } + if (composerInput) { + composerInput.disabled = !chat || Boolean(blockedReason) || Boolean(state.whatsapp.pendingAction); + composerInput.placeholder = !chat + ? 'Выберите чат' + : blockedReason || 'Написать сообщение'; + } + if (avatar) { + avatar.textContent = chat ? whatsappChatInitials(chat) : 'WA'; + avatar.style.setProperty('--whatsapp-avatar-start', start); + avatar.style.setProperty('--whatsapp-avatar-end', end); + } + if (paneCaption) { + paneCaption.textContent = state.whatsapp.mode === 'live' + ? 'Живая очередь с поддержкой AI' + : state.whatsapp.mode === 'mock' + ? 'Демо-режим, пока backend недоступен' + : 'Подключаемся к backend WhatsApp'; + } + $('whatsappChatTitle').textContent = chat ? chat.title : 'WhatsApp'; + $('whatsappChatMeta').textContent = chat + ? [whatsappThreadHandle(chat), whatsappThreadPresenceText(chat)].filter(Boolean).join(' • ') + : 'Выберите чат для начала переписки.'; + document.querySelectorAll('[data-whatsapp-filter]').forEach((button) => { + button.classList.toggle('active', button.dataset.whatsappFilter === state.whatsapp.activeFilter); + }); + syncWhatsappComposerUi(); +} + +async function loadWhatsappThreadMessages(threadId = state.whatsapp.selectedChatId, logResult = false) { + if (!featureEnabled('whatsapp')) { + return []; + } + if (!threadId) { + state.whatsapp.selectedThreadSummary = null; + renderWhatsappWorkspace(); + return []; + } + if (state.whatsapp.mode !== 'live') { + return selectedWhatsappChat()?.messages || []; + } + const chat = state.whatsapp.chats.find((item) => item.id === threadId) || null; + const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(threadId)}/messages`); + const messages = Array.isArray(data) + ? data.map((message) => normalizeWhatsappMessage(message, chat)) + : []; + updateWhatsappChatMessages(threadId, messages); + if (logResult) { + log('WhatsApp thread messages обновлены', { thread_id: threadId, messages: messages.length }); + } + return messages; +} + +async function loadWhatsappThreadSummary(threadId = state.whatsapp.selectedChatId, options = {}) { + if (!featureEnabled('whatsapp')) { + return null; + } + const { logResult = false } = options; + if (!threadId || state.whatsapp.mode !== 'live') { + state.whatsapp.selectedThreadSummary = null; + renderWhatsappWorkspace(); + return null; + } + const thread = state.whatsapp.chats.find((item) => item.id === threadId) || null; + if (!whatsappThreadSummaryAccessible(thread)) { + if (threadId === state.whatsapp.selectedChatId) { + state.whatsapp.selectedThreadSummary = null; + renderWhatsappWorkspace(); + } + return null; + } + try { + const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(threadId)}/ai-summary`); + if (threadId === state.whatsapp.selectedChatId) { + state.whatsapp.selectedThreadSummary = data || null; + renderWhatsappWorkspace(); + } + if (logResult && data) { + log('AI-сводка WhatsApp загружена', { thread_id: threadId, session_id: data.session_id }); + } + return data || null; + } catch (err) { + if (threadId === state.whatsapp.selectedChatId) { + state.whatsapp.selectedThreadSummary = null; + renderWhatsappWorkspace(); + } + if (logResult) { + log('Не удалось загрузить AI-сводку WhatsApp', { thread_id: threadId, error: err.message }); + } + return null; + } +} + +async function loadWhatsappThreads(logResult = true, options = {}) { + if (!featureEnabled('whatsapp')) { + return []; + } + const { + preserveSelection = true, + preserveOnError = false, + allowMockFallback = true, + } = options; + try { + const data = await api('whatsapp', 'integrations/whatsapp/threads'); + const existingById = new Map(state.whatsapp.chats.map((chat) => [chat.id, chat])); + const chats = (Array.isArray(data) ? data : []).map((thread) => normalizeWhatsappThread(thread, existingById.get(thread.thread_id))); + state.whatsapp.mode = 'live'; + state.whatsapp.backendError = ''; + applyWhatsappChats(chats, { preserveSelection }); + await loadWhatsappThreadMessages(state.whatsapp.selectedChatId, false); + await loadWhatsappThreadSummary(state.whatsapp.selectedChatId, { logResult: false }); + if (logResult) { + log('WhatsApp threads обновлены', { + threads: state.whatsapp.chats.length, + ai_threads: state.whatsapp.chats.filter((chat) => chat.aiState).length, + }); + } + } catch (err) { + state.whatsapp.backendError = err.message; + if (!state.whatsapp.chats.length && allowMockFallback) { + seedWhatsappChats(); + renderWhatsappWorkspace(); + if (logResult || !state.whatsapp.mockFallbackLogged) { + log('WhatsApp backend недоступен, включён demo fallback', { error: err.message }); + state.whatsapp.mockFallbackLogged = true; + } + return; + } + if (!preserveOnError) { + state.whatsapp.chats = []; + state.whatsapp.selectedChatId = ''; + state.whatsapp.selectedThreadSummary = null; + renderWhatsappWorkspace(); + } + if (logResult) { + log('Не удалось загрузить WhatsApp threads', { error: err.message }); + } + } +} + +async function selectWhatsappChat(chatId) { + if (!chatId) { + return; + } + if (chatId !== state.whatsapp.selectedChatId) { + state.whatsapp.selectedThreadSummary = null; + } + state.whatsapp.selectedChatId = chatId; + state.whatsapp.composerText = ''; + state.whatsapp.chats = state.whatsapp.chats.map((chat) => ({ + ...chat, + unreadCount: chat.id === chatId ? 0 : chat.unreadCount, + })); + renderWhatsappWorkspace(); + if (state.whatsapp.mode === 'live') { + await loadWhatsappThreadMessages(chatId, false); + await loadWhatsappThreadSummary(chatId, { logResult: false }); + } + window.requestAnimationFrame(() => { + $('whatsappComposerInput')?.focus(); + }); +} + +function setWhatsappFilter(filter) { + state.whatsapp.activeFilter = filter || 'all'; + renderWhatsappWorkspace(); +} + +async function claimWhatsappThread(options = {}) { + const { silent = false } = options; + const chat = selectedWhatsappChat(); + if (!chat) { + return null; + } + const shouldManagePending = !state.whatsapp.pendingAction; + if (shouldManagePending) { + state.whatsapp.pendingAction = 'claim'; + renderWhatsappWorkspace(); + } + try { + const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(chat))}/claim`, { + method: 'POST', + }); + upsertWhatsappThreadData(data); + await loadWhatsappThreadSummary(data.thread_id, { logResult: false }); + loadInteractions().catch(() => {}); + if (!silent) { + log('WhatsApp thread взят в работу', { thread_id: data.thread_id, interaction_id: data.interaction_id }); + } + return selectedWhatsappChat(); + } catch (err) { + if (!silent) { + log('Не удалось взять WhatsApp thread в работу', { error: err.message }); + } + throw err; + } finally { + if (shouldManagePending) { + state.whatsapp.pendingAction = ''; + renderWhatsappWorkspace(); + } + } +} + +async function returnWhatsappThreadToAi() { + const chat = selectedWhatsappChat(); + if (!chat) { + return; + } + state.whatsapp.pendingAction = 'return-ai'; + renderWhatsappWorkspace(); + try { + const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(chat))}/return-to-ai`, { + method: 'POST', + }); + upsertWhatsappThreadData(data); + await loadWhatsappThreadSummary(data.thread_id, { logResult: false }); + loadInteractions().catch(() => {}); + log('WhatsApp thread возвращён AI', { thread_id: data.thread_id, interaction_id: data.interaction_id }); + } catch (err) { + log('Не удалось вернуть WhatsApp thread AI', { error: err.message }); + } finally { + state.whatsapp.pendingAction = ''; + renderWhatsappWorkspace(); + } +} + +async function sendWhatsappMessage() { + const chat = selectedWhatsappChat(); + const text = String(state.whatsapp.composerText || '').trim(); + if (!chat || !text) { + return; + } + const blockedReason = whatsappComposerBlockedReason(chat); + if (blockedReason) { + log('Нельзя отправить WhatsApp reply', { reason: blockedReason, thread_id: whatsappChatThreadId(chat) }); + return; + } + if (state.whatsapp.mode !== 'live') { + const now = new Date().toISOString(); + const outgoing = { + id: `wa-local-${Date.now()}`, + direction: 'out', + text, + createdAt: now, + deliveryStatus: 'read', + authorType: 'human', + }; + state.whatsapp.chats = sortWhatsappChats(state.whatsapp.chats.map((item) => { + if (item.id !== chat.id) { + return item; + } + return { + ...item, + unreadCount: 0, + lastMessageAt: now, + lastMessagePreview: text, + messages: [...(item.messages || []), outgoing], + }; + })); + state.whatsapp.selectedChatId = chat.id; + state.whatsapp.composerText = ''; + renderWhatsappWorkspace(); + window.requestAnimationFrame(() => { + $('whatsappComposerInput')?.focus(); + }); + log('WhatsApp local reply saved', { chat_id: chat.id }); + return; + } + state.whatsapp.pendingAction = 'reply'; + renderWhatsappWorkspace(); + try { + let activeChat = selectedWhatsappChat() || chat; + if (state.role === 'operator' && !activeChat.claimedByUser) { + const claimedChat = await claimWhatsappThread({ silent: true }); + activeChat = claimedChat || selectedWhatsappChat() || activeChat; + } + const data = await api('whatsapp', `integrations/whatsapp/threads/${encodeURIComponent(whatsappChatThreadId(activeChat))}/messages`, { + method: 'POST', + body: JSON.stringify({ text }), + }); + const outgoing = normalizeWhatsappMessage(data, activeChat); + state.whatsapp.composerText = ''; + const nextChats = state.whatsapp.chats.map((item) => { + if (item.id !== activeChat.id) { + return item; + } + return { + ...item, + claimedByUser: item.claimedByUser || (state.role === 'operator' ? state.user : ''), + aiState: 'human_owned', + unreadCount: 0, + lastMessageAt: outgoing.createdAt, + lastMessagePreview: outgoing.text, + messages: [...(item.messages || []), outgoing], + }; + }); + applyWhatsappChats(nextChats); + await loadWhatsappThreadSummary(whatsappChatThreadId(activeChat), { logResult: false }); + log('WhatsApp reply поставлен в очередь', { + thread_id: whatsappChatThreadId(activeChat), + delivery_status: data.delivery_status || 'pending', + }); + window.setTimeout(() => { + loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true, allowMockFallback: false }); + }, 0); + } catch (err) { + log('Не удалось отправить WhatsApp reply', { error: err.message }); + } finally { + state.whatsapp.pendingAction = ''; + renderWhatsappWorkspace(); + window.requestAnimationFrame(() => { + $('whatsappComposerInput')?.focus(); + }); + } +} + +function startWhatsappPolling() { + if (!featureEnabled('whatsapp')) { + stopWhatsappPolling(); + return; + } + stopWhatsappPolling(); + state.whatsapp.pollTimer = window.setInterval(() => { + loadWhatsappThreads(false, { + preserveSelection: true, + preserveOnError: true, + allowMockFallback: state.whatsapp.mode !== 'live', + }); + }, 5000); +} + +function handleWhatsappUiAction(event) { + if (!featureEnabled('whatsapp')) { + return; + } + const action = event.currentTarget.dataset.whatsappUiAction || 'неизвестно'; + if (action === 'chats') { + return; + } + if (action === 'search-chat') { + $('whatsappSearchInput')?.focus(); + return; + } + if (action === 'new-chat') { + $('whatsappSearchInput')?.focus(); + log('Создание нового WhatsApp чата пока не подключено'); + return; + } + log('WhatsApp UI action пока без backend-привязки', { action, mode: state.whatsapp.mode }); +} + +function toggleCustomerLeadForm(forceOpen = null) { + state.customers.leadFormOpen = forceOpen === null ? !state.customers.leadFormOpen : Boolean(forceOpen); + syncCustomerFormUi(); +} + +function exportCustomersCsv() { + if (!state.customers.items.length) { + log('Нечего экспортировать: список лидов пуст.'); + return; + } + const csv = [ + ['customer_id', 'display_name', 'phone', 'source', 'status', 'score'], + ...state.customers.items.map((item, index) => [ + item.customer_id, + item.display_name, + (item.phones || []).join(', '), + customerLeadSourceMeta(item, index).label, + customerLeadStatusMeta(item, index).label, + customerLeadScore(item, index), + ]), + ] + .map((row) => row.map((value) => `"${String(value ?? '').replaceAll('"', '""')}"`).join(',')) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'leads.csv'; + link.click(); + URL.revokeObjectURL(url); + log('Экспорт лидов подготовлен', { rows: state.customers.items.length }); +} + +function statusMeta(status) { + return STATUS_META[status] || { label: status || 'неизвестно', className: 'status-closed' }; +} + +function channelLabel(channel) { + return CHANNEL_LABELS[channel] || channel || 'Канал не указан'; +} + +function formatIsoShort(value) { + if (!value) { + return 'н/д'; + } + const dt = new Date(value); + if (Number.isNaN(dt.getTime())) { + return value; + } + return dt.toLocaleString(); +} + +function renderSummaryCard(label, value, note = '') { + return ` +
            +
            ${label}
            +
            ${value}
            + ${note ? `
            ${note}
            ` : ''} +
            + `; +} + +function updateProfileMeta() { + const profileName = $('profileName'); + const profileRole = $('profileRole'); + if (!profileName || !profileRole) { + return; + } + profileName.textContent = state.fullName || state.user || 'Пользователь KonturCC'; + profileRole.textContent = ROLE_LABELS[state.role] || state.role || 'Пользователь'; +} + +function operatorHashState(hash = window.location.hash) { + const raw = String(hash || '').replace(/^#/, ''); + if (raw.startsWith('customer-profile/')) { + return { view: 'customer-profile', anchor: '', customerId: decodeURIComponent(raw.slice('customer-profile/'.length)) }; + } + if (raw === 'customer-profile') { + return { view: 'customer-profile', anchor: '', customerId: '' }; + } + if (!raw || raw === 'workspace') { + return { view: 'workspace', anchor: '', customerId: '' }; + } + if (raw === 'customers-page' || raw === 'customers') { + return { view: 'customers', anchor: '', customerId: '' }; + } + if (raw === 'messages-page' || raw === 'messages') { + return { view: 'messages', anchor: '', customerId: '' }; + } + if (raw === 'telegram-page' || raw === 'telegram') { + return { view: 'telegram', anchor: '', customerId: '' }; + } + if (raw === 'whatsapp-page' || raw === 'whatsapp') { + return { view: 'whatsapp', anchor: '', customerId: '' }; + } + if (raw === 'calls' || raw === 'calls-page' || raw === 'voice-debug' || raw === 'voice-debug-page') { + return { view: 'calls', anchor: '', customerId: '' }; + } + if (raw === 'interactions' || raw === 'integrations') { + return { view: 'workspace', anchor: raw, customerId: '' }; + } + return { view: 'workspace', anchor: '', customerId: '' }; +} + +function syncOperatorViewNav(view, anchor = '') { + document.querySelectorAll('[data-operator-view-link]').forEach((link) => { + const linkView = link.dataset.operatorViewLink; + const isCustomers = view === 'customer-profile' && linkView === 'customers'; + link.classList.toggle('active', linkView === view || isCustomers); + }); + document.querySelectorAll('[data-operator-anchor-link]').forEach((link) => { + link.classList.toggle('active', view === 'workspace' && link.dataset.operatorAnchorLink === anchor); + }); +} + +function applyOperatorViewFromHash(options = {}) { + const { scroll = false } = options; + let { view, anchor, customerId } = operatorHashState(); + const viewLink = document.querySelector(`[data-operator-view-link="${view}"]`); + if (!isOperatorViewEnabled(view) || (viewLink && (viewLink.hidden || viewLink.style.display === 'none'))) { + view = 'workspace'; + anchor = ''; + window.history.replaceState(null, '', '#workspace'); + } else if (view === 'calls' && (window.location.hash === '#voice-debug' || window.location.hash === '#voice-debug-page')) { + window.history.replaceState(null, '', '#calls'); + } + if (customerId) { + state.customers.selectedCustomerId = customerId; + } + if (view === 'customer-profile' && !selectedCustomer()) { + view = 'customers'; + window.history.replaceState(null, '', '#customers-page'); + } + Object.entries(OPERATOR_VIEW_IDS).forEach(([key, id]) => { + $(id)?.classList.toggle('hidden', key !== view); + }); + syncOperatorViewNav(view, anchor); + if (view === 'messages') { + renderMessengerWorkspace(); + startMessengerPolling(); + loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }).catch((err) => { + state.messenger.backendError = err.message || 'Не удалось загрузить сообщения.'; + renderMessengerWorkspace(); + }); + } else { + stopMessengerPolling(); + } + + const targetId = view === 'workspace' ? anchor : ''; + window.requestAnimationFrame(() => { + if (targetId && $(targetId)) { + $(targetId).scrollIntoView({ behavior: scroll ? 'smooth' : 'auto', block: 'start' }); + return; + } + window.scrollTo({ top: 0, behavior: scroll ? 'smooth' : 'auto' }); + }); +} + +function handleOperatorHashChange() { + applyOperatorViewFromHash({ scroll: true }); +} + +function renderSupervisorSummary(data) { + const agents = data?.agents || {}; + const byState = agents.by_state || {}; + const firstQueue = (data?.queues || [])[0] || {}; + const total = Number(agents.total || 0); + const ready = Number(byState.READY || 0); + const busy = Number(byState.BUSY || 0); + const queueId = firstQueue.queue_id || 'нет очереди'; + const inQueue = Number(firstQueue.in_queue || 0); + + $('supervisorSummary').innerHTML = [ + renderSummaryCard('Агентов', total, 'Текущее состояние смены'), + renderSummaryCard('Готовы', ready, 'Могут взять следующий контакт'), + renderSummaryCard('Заняты', busy, 'Сейчас в обработке'), + renderSummaryCard('Очередь', queueId, `В очереди: ${inQueue}`), + ].join(''); +} + +function renderKpiSummary(data) { + const kpi = data?.kpi || {}; + $('kpiSummary').innerHTML = [ + renderSummaryCard('SL', `${Number(kpi.SL || 0).toFixed(2)}%`, 'Доля контактов в SLA'), + renderSummaryCard('ASA', `${Number(kpi.ASA || 0).toFixed(2)} с`, 'Среднее время ожидания'), + renderSummaryCard('AHT', `${Number(kpi.AHT || 0).toFixed(2)} с`, 'Среднее время обработки'), + renderSummaryCard('Abandon', `${Number(kpi.Abandon || 0).toFixed(2)}%`, 'Потерянные обращения'), + renderSummaryCard('FCR', `${Number(kpi.FCR || 0).toFixed(2)}%`, 'Решено с первого контакта'), + ].join(''); +} + +function getDefaultAssignee() { + return $('defaultAssignee').value.trim() || DEMO_ASSIGNEE; +} + +function getDefaultEscalationQueue() { + return $('defaultEscalationQueue').value.trim() || DEMO_QUEUE; +} + +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 res = await fetch('/health'); + const data = await res.json(); + $('gatewayStatus').textContent = `Шлюз: ${data.status === 'ok' ? 'готов' : data.status}`; + } catch { + $('gatewayStatus').textContent = 'Шлюз: недоступен'; + } +} + +async function loadOperatorConfig() { + try { + const response = await fetch('/operator/config'); + const data = await response.json(); + state.features.whatsapp = Boolean(data?.features?.whatsapp); + } catch { + state.features.whatsapp = false; + } + syncWhatsappFeatureVisibility(); +} + +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(); + const password = $('sessionPassword').value; + + try { + const data = await api('auth', 'auth/login', { + method: 'POST', + body: JSON.stringify({ username: state.user, password }), + }); + 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 }); + } +} + +async function createCustomer() { + const displayName = $('customerName').value.trim(); + const phone = $('customerPhone').value.trim(); + if (!displayName) { + return; + } + + const payload = { + display_name: displayName, + phones: phone ? [phone] : [], + preferred_phone: phone || null, + tags: [], + }; + + try { + const data = await api('customer', 'customers', { + method: 'POST', + body: JSON.stringify(payload), + }); + $('interactionCustomerId').value = data.customer_id; + $('customerName').value = ''; + $('customerPhone').value = ''; + toggleCustomerLeadForm(false); + log('Клиент создан', { customer_id: data.customer_id, name: data.display_name }); + await searchCustomers(); + } catch (err) { + log('Не удалось создать клиента', { error: err.message }); + } +} + +async function searchCustomers() { + const rawQuery = $('customerQuery').value.trim(); + const q = encodeURIComponent(rawQuery); + try { + const data = await api('customer', `customers${q ? `?query=${q}` : ''}`); + if (!data.length) { + state.customers.items = []; + state.customers.page = 1; + state.customers.selectedCustomerId = ''; + renderCustomerList(); + renderUnifiedInbox(); + return; + } + if (!$('interactionCustomerId').value.trim()) { + $('interactionCustomerId').value = data[0].customer_id; + } + state.customers.items = data; + state.customers.page = 1; + const selectedExists = data.some((item) => item.customer_id === state.customers.selectedCustomerId); + state.customers.selectedCustomerId = selectedExists ? state.customers.selectedCustomerId : data[0].customer_id; + renderCustomerList(); + renderUnifiedInbox(); + } catch (err) { + state.customers.items = []; + state.customers.page = 1; + state.customers.selectedCustomerId = ''; + renderCustomerList(); + renderUnifiedInbox(); + log('Не удалось загрузить клиентов', { error: err.message }); + } +} + +async function createInteraction() { + const payload = { + channel: $('interactionChannel').value, + subject: $('interactionSubject').value.trim() || 'Операционный контакт', + customer_id: $('interactionCustomerId').value.trim() || null, + queue_id: 'q_main', + priority: 3, + }; + + try { + const data = await api('interaction', 'interactions', { + method: 'POST', + body: JSON.stringify(payload), + }); + log('Обращение создано', { interaction_id: data.interaction_id, channel: data.channel }); + await loadInteractions(); + } catch (err) { + log('Не удалось создать обращение', { error: err.message }); + } +} + +function renderInteractionCard(item) { + const meta = statusMeta(item.status); + const facebookLead = isFacebookLeadInteraction(item); + const cardClass = item.status === 'escalated' + ? 'pipeline-card escalated' + : item.status === 'closed' + ? 'pipeline-card closed' + : 'pipeline-card'; + const badges = [ + facebookLead ? 'Facebook' : '', + `${escapeHtml(channelLabel(item.channel))}`, + item.queue_id ? `${escapeHtml(item.queue_id)}` : '', + item.assigned_to ? `${escapeHtml(item.assigned_to)}` : '', + ].filter(Boolean).join(''); + + return ` +
            +
            ${badges}
            +
            +

            ${escapeHtml(item.subject || 'Без темы')}

            + ${escapeHtml(meta.label)} +
            +

            ${escapeHtml(item.interaction_id)}

            +

            Клиент: ${escapeHtml(item.customer_id || 'не привязан')}

            +

            Обновлено: ${escapeHtml(formatIsoShort(item.updated_at))}

            +
            +
            + ${facebookLead ? `` : ''} + + + +
            +
            + `; +} + +function renderInteractionBoard(items) { + const groups = { + new: [], + in_progress: [], + escalated: [], + closed: [], + }; + + items.forEach((item) => { + const bucket = groups[item.status] ? item.status : 'closed'; + groups[bucket].push(item); + }); + + return ` +
            + ${BOARD_COLUMNS.map((column) => { + const cards = groups[column.key]; + const content = cards.length + ? cards.map((item) => renderInteractionCard(item)).join('') + : '
            Пока пусто.
            '; + return ` +
            +
            +
            ${column.title}
            +
            ${cards.length}
            +
            +
            + ${content} +
            +
            + `; + }).join('')} +
            + `; +} + +function unifiedInboxSortValue(...values) { + for (const value of values) { + const parsed = new Date(value || '').getTime(); + if (Number.isFinite(parsed) && parsed > 0) { + return parsed; + } + } + return 0; +} + +function unifiedInboxInteractionBucket(item) { + if (!item || item.status === 'closed') { + return ''; + } + if (item.status === 'escalated') { + return 'escalated'; + } + if (String(item.assigned_to || '').trim() === String(state.user || '').trim()) { + return 'mine'; + } + if (item.status === 'new' || !String(item.assigned_to || '').trim()) { + return 'new'; + } + return ''; +} + +function unifiedInboxTelegramBucket(thread) { + if (!thread || thread.status === 'closed') { + return ''; + } + if (['handoff_required', 'human_owned'].includes(String(thread.ai_state || '').trim())) { + return 'ai'; + } + if (String(thread.claimed_by_user || '').trim() === String(state.user || '').trim()) { + return 'mine'; + } + return 'new'; +} + +function renderUnifiedInboxBadge(label, className = '') { + if (!label) { + return ''; + } + return `${escapeHtml(label)}`; +} + +function unifiedInboxActionLabel(item) { + if (item.kind === 'telegram') { + return 'Открыть чат'; + } + if (item.kind === 'call') { + return 'Открыть звонок'; + } + return 'К обращению'; +} + +function buildUnifiedInboxInteractionItem(item) { + const bucket = unifiedInboxInteractionBucket(item); + if (!bucket) { + return null; + } + const facebookLead = isFacebookLeadInteraction(item); + const badges = [ + facebookLead ? renderUnifiedInboxBadge('Facebook', 'facebook') : '', + renderUnifiedInboxBadge(channelLabel(item.channel), 'channel'), + item.queue_id ? renderUnifiedInboxBadge(item.queue_id, 'queue') : '', + item.assigned_to ? renderUnifiedInboxBadge(item.assigned_to, 'owner') : '', + ].filter(Boolean); + return { + key: `interaction:${item.interaction_id}`, + kind: 'interaction', + bucket, + sortValue: unifiedInboxSortValue(item.updated_at, item.created_at), + title: item.subject || 'Обращение без темы', + subtitle: item.interaction_id, + badges, + metaLines: [ + `Клиент: ${customerDisplayName(item.customer_id || '')}`, + `Статус: ${statusMeta(item.status).label}`, + `Обновлено: ${formatIsoShort(item.updated_at || item.created_at)}`, + ], + customerId: item.customer_id || '', + interactionId: item.interaction_id, + canReply: facebookLead, + }; +} + +function buildUnifiedInboxTelegramItem(thread) { + const bucket = unifiedInboxTelegramBucket(thread); + if (!bucket) { + return null; + } + const customerId = customerIdForInteractionId(thread.interaction_id); + const aiMeta = telegramThreadAiStateMeta(thread); + const unreadCount = telegramThreadUnreadCount(thread); + const badges = [ + renderUnifiedInboxBadge('Telegram', 'channel'), + unreadCount > 0 ? renderUnifiedInboxBadge(`${unreadCount} new`, 'queue') : '', + aiMeta ? renderUnifiedInboxBadge(aiMeta.label, aiMeta.tone) : '', + thread.claimed_by_user ? renderUnifiedInboxBadge(thread.claimed_by_user, 'owner') : '', + ].filter(Boolean); + return { + key: `telegram:${thread.thread_id}`, + kind: 'telegram', + bucket, + sortValue: unifiedInboxSortValue(thread.last_message_at, thread.updated_at, thread.created_at), + title: telegramThreadDisplayName(thread), + subtitle: telegramThreadSummary(thread), + badges, + metaLines: [ + `Клиент: ${customerDisplayName(customerId)}`, + thread.last_message_preview ? `Последнее: ${thread.last_message_preview}` : 'Последнее сообщение пока не загружено', + `Статус: ${telegramThreadHeaderPresence(thread)}`, + ], + customerId, + interactionId: thread.interaction_id || '', + threadId: thread.thread_id, + }; +} + +function buildUnifiedInboxCallItem(item) { + const summary = voiceSummaryForItem(item); + const aiMeta = voiceAiStatusMeta(item); + const interaction = interactionById(item.interaction_id || ''); + const customerId = interaction?.customer_id || ''; + const caller = voiceCustomerDisplayName(item, summary); + const nameStatusMeta = voiceCustomerNameStatusMeta(summary?.customer_name_status); + const badges = [ + renderUnifiedInboxBadge('Голос', 'channel'), + renderUnifiedInboxBadge(telephonyLabel(item.telephony_status), 'assignee'), + aiMeta ? renderUnifiedInboxBadge(aiMeta.label, aiMeta.className) : '', + nameStatusMeta ? renderUnifiedInboxBadge(nameStatusMeta.shortLabel, nameStatusMeta.className) : '', + item.claimed_by_user ? renderUnifiedInboxBadge(item.claimed_by_user, 'owner') : '', + ].filter(Boolean); + return { + key: `call:${item.call_id}`, + kind: 'call', + bucket: 'calls', + sortValue: unifiedInboxSortValue(item.started_at, item.updated_at), + title: caller, + subtitle: voiceCustomerCallSubtitle(item), + badges, + metaLines: [ + `Клиент: ${customerDisplayName(customerId)}`, + `Контакт: ${voiceCustomerCallSubtitle(item)}`, + summary ? `Имя: ${voiceCustomerNameStateLine(summary) || 'без подтверждения'}` : '', + `Обращение: ${item.interaction_id || 'не найдено'}`, + `Начат: ${formatIsoShort(item.started_at || item.connected_at)}`, + item.ai_handoff_reason ? `AI: ${item.ai_handoff_reason}` : `Статус: ${telephonyLabel(item.telephony_status)}`, + ].filter(Boolean), + customerId, + interactionId: item.interaction_id || '', + callId: item.call_id, + }; +} + +function buildUnifiedInboxCollections() { + const collections = { + new: [], + mine: [], + ai: [], + escalated: [], + calls: [], + }; + const representedInteractionIds = new Set(); + + state.telegram.threads.forEach((thread) => { + if (thread?.interaction_id && thread.status !== 'closed') { + representedInteractionIds.add(thread.interaction_id); + } + const item = buildUnifiedInboxTelegramItem(thread); + if (item) { + collections[item.bucket].push(item); + } + }); + + state.liveCalls.items.forEach((call) => { + if (call?.interaction_id) { + representedInteractionIds.add(call.interaction_id); + } + const item = buildUnifiedInboxCallItem(call); + if (item) { + collections.calls.push(item); + } + }); + + state.interactions.forEach((item) => { + if (representedInteractionIds.has(item.interaction_id)) { + return; + } + const inboxItem = buildUnifiedInboxInteractionItem(item); + if (inboxItem) { + collections[inboxItem.bucket].push(inboxItem); + } + }); + + Object.values(collections).forEach((items) => { + items.sort((left, right) => right.sortValue - left.sortValue); + }); + return collections; +} + +function renderUnifiedInboxCard(item) { + const actions = [ + ``, + item.customerId ? `` : '', + item.canReply ? `` : '', + ].filter(Boolean).join(''); + + return ` +
            +
            ${item.badges.join('')}
            +
            +

            ${escapeHtml(item.title)}

            +
            +

            ${escapeHtml(item.subtitle)}

            + ${item.metaLines.map((line) => `

            ${escapeHtml(line)}

            `).join('')} +
            +
            + ${actions} +
            +
            + `; +} + +function renderUnifiedInbox() { + const board = $('unifiedInboxBoard'); + const summary = $('unifiedInboxSummary'); + if (!board || !summary) { + return; + } + const collections = buildUnifiedInboxCollections(); + const total = Object.values(collections).reduce((count, items) => count + items.length, 0); + summary.innerHTML = [ + renderSummaryCard('В очереди', String(total), 'Все задачи без дублей между каналами'), + renderSummaryCard('Новые', String(collections.new.length), 'Свободные обращения и чаты'), + renderSummaryCard('Мои', String(collections.mine.length), 'Закреплено за текущим пользователем'), + renderSummaryCard('AI handoff', String(collections.ai.length), 'Диалоги, где AI позвал человека'), + renderSummaryCard('Звонки', String(collections.calls.length), 'Активные голосовые разговоры'), + renderSummaryCard('Эскалации', String(collections.escalated.length), 'Очередь второй линии и спорные кейсы'), + ].join(''); + board.innerHTML = ` +
            + ${UNIFIED_INBOX_COLUMNS.map((column) => ` +
            +
            +
            ${escapeHtml(column.title)}
            +
            ${collections[column.key].length}
            +
            +
            + ${collections[column.key].length + ? collections[column.key].map((item) => renderUnifiedInboxCard(item)).join('') + : `
            ${escapeHtml(column.emptyMessage)}
            `} +
            +
            + `).join('')} +
            + `; +} + +function focusLiveCall(callId) { + if (!callId) { + return; + } + const activeCall = state.liveCalls.items.find((item) => item.call_id === callId) || null; + const relatedCall = activeCall || state.liveCalls.recentItems.find((item) => item.call_id === callId) || null; + const select = $('liveCallIdSelect'); + if (select) { + select.value = activeCall ? callId : ''; + } + state.liveCalls.selectedCallId = callId; + renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems); + syncLiveCallActionButtons(); + ensureVoiceAiSummaryLoaded(relatedCall); +} + +function handleLiveCallTableClick(event) { + const button = event.target.closest('[data-live-call-action]'); + if (!button) { + return; + } + const callId = button.dataset.callId || ''; + if (!callId) { + return; + } + if (button.dataset.liveCallAction === 'edit-name') { + openLiveCallNameEditor(callId, 'panel'); + return; + } + if (button.dataset.liveCallAction === 'customer') { + const customerId = button.dataset.customerId || ''; + if (customerId) { + openCustomerProfile(customerId); + } + } +} + +async function openUnifiedInboxItem(button) { + const kind = button.dataset.inboxKind || ''; + const interactionId = button.dataset.interactionId || ''; + const threadId = button.dataset.threadId || ''; + const callId = button.dataset.callId || ''; + const customerId = button.dataset.customerId || ''; + + if (kind === 'telegram') { + window.location.hash = '#messages'; + await selectMessengerConversation(messengerConversationId('telegram', threadId)); + return; + } + if (kind === 'call') { + focusLiveCall(callId); + window.location.hash = '#calls'; + return; + } + if (customerId) { + $('interactionCustomerId').value = customerId; + } + window.location.hash = '#interactions'; + log('Открыто обращение из единой очереди', { interaction_id: interactionId, customer_id: customerId || '-' }); +} + +function handleUnifiedInboxClick(event) { + const button = event.target.closest('[data-inbox-action]'); + if (!button) { + return; + } + if (button.dataset.inboxAction === 'customer') { + openCustomerProfile(button.dataset.customerId || ''); + return; + } + openUnifiedInboxItem(button).catch((err) => { + log('Не удалось открыть элемент единой очереди', { error: err.message }); + }); +} + +async function refreshUnifiedInbox() { + await Promise.all([ + searchCustomers(), + loadInteractions(), + loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }), + loadLiveCalls(false, { preserveStateOnError: true }), + selectedCustomer() ? ensureCustomerHistoryLoaded(selectedCustomer().customer_id, { force: true }) : Promise.resolve(null), + ]); + log('Единая очередь обновлена', { total: Object.values(buildUnifiedInboxCollections()).reduce((count, items) => count + items.length, 0) }); +} + +function telephonyLabel(value) { + return LIVE_TELEPHONY_LABELS[value] || value || 'неизвестно'; +} + +function selectedLiveCallId() { + const value = $('liveCallIdSelect')?.value || ''; + return value.trim(); +} + +function selectedLiveCallItem() { + const callId = selectedLiveCallId(); + return state.liveCalls.items.find((item) => item.call_id === callId) || null; +} + +function liveCallById(callId) { + if (!callId) { + return null; + } + return state.liveCalls.items.find((item) => item.call_id === callId) + || state.liveCalls.recentItems.find((item) => item.call_id === callId) + || null; +} + +function liveCallCustomerId(item) { + return interactionById(item?.interaction_id || '')?.customer_id || ''; +} + +function closeCustomerProfileNameEditor() { + state.customers.nameEditor.open = false; + state.customers.nameEditor.customerId = ''; + state.customers.nameEditor.draft = ''; + state.customers.nameEditor.saving = false; + state.customers.nameEditor.error = ''; + renderCustomerSpotlight(); +} + +function openCustomerProfileNameEditor(customerId = state.customers.selectedCustomerId) { + const customer = state.customers.items.find((item) => item.customer_id === customerId) || null; + if (!customer) { + return; + } + state.customers.nameEditor.open = true; + state.customers.nameEditor.customerId = customer.customer_id; + state.customers.nameEditor.draft = customer.display_name || ''; + state.customers.nameEditor.saving = false; + state.customers.nameEditor.error = ''; + state.customers.nameEditor.flash = ''; + renderCustomerSpotlight(); +} + +function closeLiveCallNameEditor() { + state.liveCalls.nameEditor.open = false; + state.liveCalls.nameEditor.callId = ''; + state.liveCalls.nameEditor.customerId = ''; + state.liveCalls.nameEditor.draft = ''; + state.liveCalls.nameEditor.saving = false; + state.liveCalls.nameEditor.error = ''; + updateLiveCallNameEditorsUi(); +} + +function openLiveCallNameEditor(callId, mode = 'panel') { + const item = liveCallById(callId); + const customerId = liveCallCustomerId(item); + if (!item || !customerId) { + log('Нельзя исправить имя: звонок не привязан к клиенту', { call_id: callId || '-' }); + return; + } + state.liveCalls.nameEditor.open = true; + state.liveCalls.nameEditor.mode = mode; + state.liveCalls.nameEditor.callId = item.call_id; + state.liveCalls.nameEditor.customerId = customerId; + state.liveCalls.nameEditor.draft = voiceCustomerDisplayName(item); + state.liveCalls.nameEditor.saving = false; + state.liveCalls.nameEditor.error = ''; + if (mode === 'panel') { + focusLiveCall(item.call_id); + } + updateLiveCallNameEditorsUi(); +} + +function applyCustomerNamePatchLocally({ customerId, callId, displayName }) { + const normalizedName = String(displayName || '').trim(); + if (!customerId || !normalizedName) { + return; + } + state.customers.items = state.customers.items.map((item) => ( + item.customer_id === customerId ? { ...item, display_name: normalizedName } : item + )); + const history = state.customers.historyById[customerId]; + if (history?.customer) { + history.customer.display_name = normalizedName; + } + const interactionIds = new Set( + state.interactions + .filter((item) => item.customer_id === customerId) + .map((item) => item.interaction_id) + .filter(Boolean), + ); + const patchCall = (item) => { + if (!item) { + return item; + } + const matchesCall = callId && item.call_id === callId; + const matchesCustomer = interactionIds.has(item.interaction_id); + if (!matchesCall && !matchesCustomer) { + return item; + } + return { + ...item, + caller_name: normalizedName, + }; + }; + state.liveCalls.items = state.liveCalls.items.map(patchCall); + state.liveCalls.recentItems = state.liveCalls.recentItems.map(patchCall); + if (history?.live_calls) { + history.live_calls = history.live_calls.map((item) => ({ + ...item, + caller_name: interactionIds.has(item.interaction_id) || item.call_id === callId ? normalizedName : item.caller_name, + })); + } + const affectedCallIds = new Set( + [...state.liveCalls.items, ...state.liveCalls.recentItems] + .filter((item) => interactionIds.has(item.interaction_id) || item.call_id === callId) + .map((item) => item.call_id) + .filter(Boolean), + ); + affectedCallIds.forEach((id) => { + if (!state.liveCalls.aiSummaries[id]) { + return; + } + state.liveCalls.aiSummaries[id] = { + ...state.liveCalls.aiSummaries[id], + customer_name_status: 'name_obtained', + customer_name_value: normalizedName, + customer_name_source: 'manual', + }; + }); + if (state.liveCalls.nameEditor.customerId === customerId && !state.liveCalls.nameEditor.saving) { + state.liveCalls.nameEditor.draft = normalizedName; + } + if (state.customers.nameEditor.customerId === customerId && !state.customers.nameEditor.saving) { + state.customers.nameEditor.draft = normalizedName; + } + renderCustomerList(); + refreshVoiceSummaryDependentViews(); +} + +async function persistCustomerDisplayName({ customerId, displayName, callId = '' }) { + const data = await api('customer', `customers/${encodeURIComponent(customerId)}`, { + method: 'PATCH', + body: JSON.stringify({ display_name: displayName, source: 'manual' }), + }); + const savedName = data?.display_name || displayName; + applyCustomerNamePatchLocally({ + customerId, + callId, + displayName: savedName, + }); + ensureCustomerHistoryLoaded(customerId, { force: true }).catch(() => {}); + if (callId) { + loadVoiceAiSummary(callId, { force: true, silent: true }).catch(() => {}); + } + refreshLiveCallsInBackground(); + return savedName; +} + +async function saveLiveCallCustomerName() { + const { customerId, callId } = state.liveCalls.nameEditor; + const displayName = String(state.liveCalls.nameEditor.draft || '').trim().replace(/\s+/g, ' '); + if (!customerId || !callId) { + return; + } + if (displayName.length < 2) { + state.liveCalls.nameEditor.error = 'Введите имя клиента минимум из 2 символов.'; + updateLiveCallNameEditorsUi(); + return; + } + state.liveCalls.nameEditor.saving = true; + state.liveCalls.nameEditor.error = ''; + updateLiveCallNameEditorsUi(); + try { + const savedName = await persistCustomerDisplayName({ customerId, callId, displayName }); + const data = { display_name: savedName }; + closeLiveCallNameEditor(); + log('Имя клиента обновлено оператором', { customer_id: customerId, call_id: callId, name: data?.display_name || displayName }); + } catch (err) { + state.liveCalls.nameEditor.saving = false; + state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.'; + updateLiveCallNameEditorsUi(); + } +} + +async function saveCustomerProfileName() { + const customerId = state.customers.nameEditor.customerId || state.customers.selectedCustomerId; + const displayName = String(state.customers.nameEditor.draft || '').trim().replace(/\s+/g, ' '); + if (!customerId) { + return; + } + if (displayName.length < 2) { + state.customers.nameEditor.error = 'Введите имя клиента минимум из 2 символов.'; + renderCustomerSpotlight(); + return; + } + const currentName = String(selectedCustomer()?.display_name || '').trim(); + if (displayName === currentName) { + closeCustomerProfileNameEditor(); + return; + } + state.customers.nameEditor.saving = true; + state.customers.nameEditor.error = ''; + renderCustomerSpotlight(); + try { + const savedName = await persistCustomerDisplayName({ customerId, displayName }); + state.customers.nameEditor.flash = 'Сохранено'; + closeCustomerProfileNameEditor(); + log('Имя клиента обновлено из профиля', { customer_id: customerId, name: savedName }); + } catch (err) { + state.customers.nameEditor.saving = false; + state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.'; + renderCustomerSpotlight(); + } +} + +function liveCallNameEditorMeta(item, customerId) { + if (!item || !customerId) { + return 'Выберите звонок, связанный с клиентом, чтобы исправить имя.'; + } + return `${voiceCustomerCallSubtitle(item)} • клиент ${customerId}`; +} + +function updateLiveCallNameEditorsUi() { + const panel = $('liveCallNameEditor'); + const panelInput = $('liveCallNameInput'); + const panelMeta = $('liveCallNameEditorMeta'); + const panelStatus = $('liveCallNameStatus'); + const panelSave = $('liveCallNameSaveBtn'); + const panelCancel = $('liveCallNameCancelBtn'); + const popup = $('browserPhoneNameEditor'); + const popupInput = $('browserPhoneNameInput'); + const popupMeta = $('browserPhoneNameHint'); + const popupStatus = $('browserPhoneNameStatus'); + const popupSave = $('browserPhoneNameSaveBtn'); + const popupCancel = $('browserPhoneNameCancelBtn'); + const editor = state.liveCalls.nameEditor; + const item = liveCallById(editor.callId); + const meta = liveCallNameEditorMeta(item, editor.customerId); + const statusText = editor.error || (editor.saving ? 'Сохраняем имя клиента...' : ''); + + if (panel) { + const panelVisible = editor.open && editor.mode === 'panel'; + panel.classList.toggle('hidden', !panelVisible); + if (panelInput) { + if (panelInput.value !== editor.draft) { + panelInput.value = editor.draft; + } + panelInput.disabled = editor.saving; + } + if (panelMeta) { + panelMeta.textContent = meta; + } + if (panelStatus) { + panelStatus.textContent = statusText; + panelStatus.classList.toggle('error', Boolean(editor.error)); + } + if (panelSave) { + panelSave.disabled = editor.saving || !editor.customerId; + panelSave.textContent = editor.saving ? 'Сохраняем...' : 'Сохранить имя'; + } + if (panelCancel) { + panelCancel.disabled = editor.saving; + } + } + + if (popup) { + const popupVisible = editor.open && editor.mode === 'popup'; + popup.classList.toggle('hidden', !popupVisible); + if (popupInput) { + if (popupInput.value !== editor.draft) { + popupInput.value = editor.draft; + } + popupInput.disabled = editor.saving; + } + if (popupMeta) { + popupMeta.textContent = meta; + } + if (popupStatus) { + popupStatus.textContent = statusText; + popupStatus.classList.toggle('hidden', !statusText); + popupStatus.classList.toggle('error', Boolean(editor.error)); + } + if (popupSave) { + popupSave.disabled = editor.saving || !editor.customerId; + popupSave.textContent = editor.saving ? 'Сохраняем...' : 'Сохранить имя'; + } + if (popupCancel) { + popupCancel.disabled = editor.saving; + } + } +} + +function isClaimableLiveCall(item) { + if (!item) { + return false; + } + return !item.claimed_by_user && ['ringing', 'claimed', 'connected'].includes(item.telephony_status); +} + +function canControlLiveCall(item) { + if (!item) { + return false; + } + return ['claimed', 'connected'].includes(item.telephony_status); +} + +function terminalActionLabel(item) { + const value = item?.terminal_action || ''; + if (value === 'blind-transfer') { + return 'Передан'; + } + if (value === 'hangup') { + return 'Завершён оператором'; + } + return 'Завершён'; +} + +function voiceAiStatusMeta(item) { + const stateKey = String(item?.ai_state || '').trim(); + if (!stateKey) { + return null; + } + return VOICE_AI_STATE_META[stateKey] || { label: stateKey, className: 'ai-muted' }; +} + +function liveCallHasAi(item) { + return Boolean( + item?.voice_session_id + || item?.ai_session_id + || item?.ai_state + || item?.ai_handoff_reason + || item?.ai_last_model_at, + ); +} + +function shouldLoadVoiceAiSummary(item) { + if (!item || !liveCallHasAi(item)) { + return false; + } + return ['handoff_requested', 'handoff_required', 'human_owned', 'error', 'closed'].includes(item.ai_state || '') + || Boolean(item.ai_handoff_reason); +} + +function voiceAiSummaryForCall(callId) { + return state.liveCalls.aiSummaries[callId] || null; +} + +function voiceAiSummaryPending(callId) { + return Boolean(state.liveCalls.aiSummaryPending[callId]); +} + +async function loadVoiceAiSummary(callId, options = {}) { + if (!callId || voiceAiSummaryPending(callId)) { + return voiceAiSummaryForCall(callId); + } + const { force = false, silent = false } = options; + if (!force && Object.prototype.hasOwnProperty.call(state.liveCalls.aiSummaries, callId)) { + return state.liveCalls.aiSummaries[callId]; + } + state.liveCalls.aiSummaryPending[callId] = true; + try { + const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/ai-summary`); + state.liveCalls.aiSummaries[callId] = data || null; + return state.liveCalls.aiSummaries[callId]; + } catch (err) { + if (!silent) { + log('Не удалось загрузить сводку AI по звонку', { call_id: callId, error: err.message }); + } + state.liveCalls.aiSummaries[callId] = null; + return null; + } finally { + delete state.liveCalls.aiSummaryPending[callId]; + refreshVoiceSummaryDependentViews(); + } +} + +function ensureVoiceAiSummaryLoaded(item) { + if (!shouldLoadVoiceAiSummary(item)) { + return; + } + const callId = item.call_id || ''; + if (!callId || voiceAiSummaryPending(callId) || Object.prototype.hasOwnProperty.call(state.liveCalls.aiSummaries, callId)) { + return; + } + window.setTimeout(() => { + loadVoiceAiSummary(callId, { silent: true }); + }, 0); +} + +function renderVoiceAiSummaryField(label, value) { + if (!value) { + return ''; + } + return ` +
            +
            ${escapeHtml(label)}
            +
            ${escapeHtml(value)}
            +
            + `; +} + +function formatVoiceCustomerNameStatus(status) { + switch (String(status || '').trim()) { + case 'name_obtained': + return 'Подтверждено'; + case 'name_followup_required': + return 'Нужно уточнить'; + case 'name_not_obtained': + return 'Не подтверждено'; + default: + return String(status || '').trim(); + } +} + +function voiceCustomerNameStatusMeta(status) { + switch (String(status || '').trim()) { + case 'name_obtained': + return { label: 'Имя подтверждено', shortLabel: 'Имя подтверждено', className: 'name-confirmed' }; + case 'name_followup_required': + return { label: 'Имя нужно уточнить', shortLabel: 'Уточнить имя', className: 'name-followup' }; + case 'name_not_obtained': + return { label: 'Имя не подтверждено', shortLabel: 'Без подтверждения', className: 'name-missing' }; + default: { + const fallback = String(status || '').trim(); + return fallback ? { label: fallback, shortLabel: fallback, className: 'name-missing' } : null; + } + } +} + +function formatVoiceCustomerNameSource(source) { + switch (String(source || '').trim()) { + case 'known_customer': + return 'Из карточки клиента'; + case 'voice_start': + return 'Стартовый этап'; + case 'voice_followup': + return 'Уточнил AI'; + case 'manual': + return 'Оператор'; + case 'external_identity': + return 'Из voice identity'; + default: + return String(source || '').trim(); + } +} + +function formatVoiceStartLanguage(language) { + switch (String(language || '').trim()) { + case 'ru': + return 'Русский'; + case 'kz': + return 'Қазақша'; + default: + return String(language || '').trim(); + } +} + +function voiceSummaryForItem(item) { + const callId = String(item?.call_id || '').trim(); + return callId ? voiceAiSummaryForCall(callId) : null; +} + +function voiceCustomerDisplayName(item, summary = voiceSummaryForItem(item)) { + const preferredName = String(summary?.customer_name_value || '').trim(); + if (preferredName) { + return preferredName; + } + const callerName = String(item?.caller_name || '').trim(); + if (callerName) { + return callerName; + } + const callerNumber = String(item?.caller_number || '').trim(); + if (callerNumber) { + return callerNumber; + } + const callId = String(item?.call_id || '').trim(); + if (callId) { + return callId; + } + return 'Неизвестный абонент'; +} + +function voiceCustomerCallSubtitle(item) { + const parts = [ + String(item?.caller_number || '').trim(), + String(item?.call_id || '').trim(), + ].filter(Boolean); + return parts.join(' • ') || 'Звонок без номера'; +} + +function voiceCustomerNameStateLine(summary) { + if (!summary) { + return ''; + } + const parts = []; + const statusMeta = voiceCustomerNameStatusMeta(summary.customer_name_status); + if (statusMeta?.label) { + parts.push(statusMeta.label); + } + const sourceLabel = formatVoiceCustomerNameSource(summary.customer_name_source); + if (sourceLabel) { + parts.push(sourceLabel); + } + const languageLabel = formatVoiceStartLanguage(summary.voice_start_language); + if (languageLabel) { + parts.push(languageLabel); + } + return parts.join(' • '); +} + +function voiceCustomerIncomingMeta(item, summary = voiceSummaryForItem(item)) { + const parts = []; + const callerNumber = String(item?.caller_number || '').trim(); + if (callerNumber) { + parts.push(`Номер: ${callerNumber}`); + } + const nameState = voiceCustomerNameStateLine(summary); + if (nameState) { + parts.push(nameState); + } + return parts.join(' • ') || `call_id: ${String(item?.call_id || '—').trim() || '—'}`; +} + +function refreshVoiceSummaryDependentViews() { + updateLiveCallSelector(state.liveCalls.items); + renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems); + renderUnifiedInbox(); + updateBrowserPhoneUi(); +} + +function renderVoiceAiTranscript(summary) { + const segments = Array.isArray(summary?.transcript_segments) + ? summary.transcript_segments.filter((segment) => segment && String(segment.text || '').trim()) + : []; + if (!segments.length) { + return ''; + } + return ` +
            +
            +
            Транскрипт разговора
            +
            ${escapeHtml(`${segments.length} реплик`)}
            +
            +
            + ${segments + .map((segment) => { + const speaker = segment?.speaker === 'assistant' ? 'AI' : 'Клиент'; + const lineClass = segment?.speaker === 'assistant' ? 'assistant' : 'caller'; + const timeLabel = formatIsoShort(segment?.created_at || ''); + const interrupted = segment?.interrupted && segment?.speaker === 'assistant' ? ' • прерван' : ''; + return ` +
            +
            +
            ${escapeHtml(`${speaker}${interrupted}`)}
            +
            ${escapeHtml(timeLabel)}
            +
            +
            ${escapeHtml(segment?.text || '')}
            +
            + `; + }) + .join('')} +
            +
            + `; +} + +function renderVoiceAiSummary(summary, pending = false) { + if (!summary && !pending) { + return ''; + } + if (pending && !summary) { + return ` +
            +
            +
            +
            Сводка AI
            +
            Подтягиваем контекст AI по текущему звонку...
            +
            +
            +
            + `; + } + const statusClass = summary?.status_tone === 'handoff' ? 'handoff' : 'answered'; + return ` +
            +
            +
            +
            Сводка AI
            +
            Краткий контекст передачи до ручной обработки звонка.
            +
            +
            +
            ${escapeHtml(summary?.status_label || 'AI-сводка')}
            +
            ${escapeHtml(formatIsoShort(summary?.generated_at || ''))}
            +
            +
            +
            + ${renderVoiceAiSummaryField('Имя клиента', summary?.customer_name_value)} + ${renderVoiceAiSummaryField('Статус имени', formatVoiceCustomerNameStatus(summary?.customer_name_status))} + ${renderVoiceAiSummaryField('Источник имени', formatVoiceCustomerNameSource(summary?.customer_name_source))} + ${renderVoiceAiSummaryField('Язык старта', formatVoiceStartLanguage(summary?.voice_start_language))} + ${renderVoiceAiSummaryField('Запрос клиента', summary?.customer_request_text)} + ${renderVoiceAiSummaryField('Что сделал AI', summary?.ai_outcome_text)} + ${renderVoiceAiSummaryField('Причина передачи', summary?.handoff_reason)} + ${renderVoiceAiSummaryField('Следующий шаг', summary?.recommended_next_step)} +
            + ${renderVoiceAiTranscript(summary)} +
            + `; +} + +function describeLiveCallError(message) { + const text = String(message || '').trim(); + const lower = text.toLowerCase(); + if (!text) { + return 'Операция по звонку не выполнена.'; + } + if (lower.includes('already ended') || lower.includes('channel_already_closed')) { + return 'Звонок уже завершён.'; + } + if (lower.includes('already claimed by another operator')) { + return 'Звонок уже закреплён другим оператором.'; + } + if (lower.includes('unable to resolve active channel')) { + return 'Asterisk не смог найти активный канал звонка.'; + } + if (lower.includes('operator_extension is required')) { + return 'Укажите внутренний номер оператора для этого действия.'; + } + if (lower.includes('target extension')) { + return 'Указанный target недоступен для передачи.'; + } + if (lower.includes('blind transfer failed')) { + return 'Передача звонка не выполнена.'; + } + if (lower.includes('hangup failed')) { + return 'Не удалось завершить звонок.'; + } + if (lower.includes('claim failed')) { + return 'Не удалось принять звонок в работу.'; + } + return text; +} + +function browserPhoneConfig() { + return state.browserPhone.config || null; +} + +function browserPhoneLibraryAvailable() { + return Boolean(window.SIP && window.SIP.UserAgent); +} + +function browserPhoneAudioElement() { + return $('browserPhoneRemoteAudio'); +} + +function browserPhoneAudioConstraints() { + if (state.browserPhone.micDeviceId) { + return { audio: { deviceId: { exact: state.browserPhone.micDeviceId } }, video: false }; + } + return { audio: true, video: false }; +} + +function browserPhoneIncomingLabel(session) { + const remote = session?.remoteIdentity; + const name = remote?.displayName || remote?.uri?.user || remote?.uri?.toString?.() || 'неизвестно'; + return `Входящий звонок: ${name}`; +} + +function browserPhoneAudioContext() { + if (state.browserPhone.ringtoneContext) { + return state.browserPhone.ringtoneContext; + } + const AudioContextCtor = window.AudioContext || window.webkitAudioContext; + if (!AudioContextCtor) { + return null; + } + state.browserPhone.ringtoneContext = new AudioContextCtor(); + return state.browserPhone.ringtoneContext; +} + +async function ensureBrowserPhoneAudioContextResumed() { + const context = browserPhoneAudioContext(); + if (!context) { + return null; + } + if (context.state === 'suspended') { + await context.resume().catch(() => {}); + } + return context; +} + +function playBrowserPhoneRingtoneBurst(context) { + if (!context) { + return; + } + const tones = [ + { offset: 0, duration: 0.18, frequency: 880 }, + { offset: 0.24, duration: 0.18, frequency: 660 }, + ]; + tones.forEach((tone) => { + const oscillator = context.createOscillator(); + const gain = context.createGain(); + const startAt = context.currentTime + tone.offset; + const stopAt = startAt + tone.duration; + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(tone.frequency, startAt); + gain.gain.setValueAtTime(0.0001, startAt); + gain.gain.exponentialRampToValueAtTime(0.14, startAt + 0.02); + gain.gain.exponentialRampToValueAtTime(0.0001, stopAt); + oscillator.connect(gain); + gain.connect(context.destination); + oscillator.start(startAt); + oscillator.stop(stopAt + 0.02); + }); +} + +async function startBrowserPhoneRingtone() { + if (state.browserPhone.ringtoneTimer) { + return; + } + const context = await ensureBrowserPhoneAudioContextResumed(); + if (!context) { + return; + } + state.browserPhone.ringtoneActive = true; + playBrowserPhoneRingtoneBurst(context); + state.browserPhone.ringtoneTimer = window.setInterval(() => { + playBrowserPhoneRingtoneBurst(context); + }, 2200); + updateBrowserPhoneUi(); +} + +function stopBrowserPhoneRingtone() { + if (state.browserPhone.ringtoneTimer) { + window.clearInterval(state.browserPhone.ringtoneTimer); + state.browserPhone.ringtoneTimer = null; + } + if (state.browserPhone.ringtoneActive) { + state.browserPhone.ringtoneActive = false; + updateBrowserPhoneUi(); + } +} + +function syncBrowserPhoneSpeakerSink() { + const element = browserPhoneAudioElement(); + if (!element || !state.browserPhone.speakerDeviceId) { + return; + } + if (typeof element.setSinkId !== 'function') { + return; + } + element.setSinkId(state.browserPhone.speakerDeviceId).catch(() => {}); +} + +function setBrowserPhoneStatus(text) { + state.browserPhone.status = text; + updateBrowserPhoneUi(); +} + +function setBrowserPhoneWarning(text = '') { + state.browserPhone.warning = text; + updateBrowserPhoneUi(); +} + +function toggleBrowserPhoneSettings() { + state.browserPhone.settingsOpen = !state.browserPhone.settingsOpen; + updateBrowserPhoneUi(); +} + +function browserPhoneStatusState() { + const config = browserPhoneConfig(); + const hasSession = Boolean(state.browserPhone.session); + const popupPhase = browserPhonePopupPhase(); + if (!config) { + return 'unavailable'; + } + if (!config.enabled) { + return 'disabled'; + } + if (state.browserPhone.connecting && popupPhase === 'idle') { + return 'connecting'; + } + if (popupPhase === 'error') { + return 'error'; + } + if (popupPhase === 'ending') { + return 'ending'; + } + if (popupPhase === 'incoming') { + return 'incoming'; + } + if (popupPhase === 'connecting') { + return 'connecting'; + } + if (popupPhase === 'in-call' || hasSession) { + return 'active'; + } + if (state.browserPhone.connected) { + return 'registered'; + } + return 'offline'; +} + +function browserPhoneStatusLabel() { + const labels = { + unavailable: 'Недоступен', + disabled: 'Отключен', + connecting: 'Подключение', + ending: 'Завершаем', + error: 'Нужно завершить', + incoming: 'Входящий звонок', + active: 'В разговоре', + registered: 'Зарегистрирован', + offline: 'Не подключен', + }; + return labels[browserPhoneStatusState()] || state.browserPhone.status || 'Не подключен'; +} + +function browserPhonePopupPhase() { + if (state.browserPhone.callPhase === 'error') { + return 'error'; + } + if (state.browserPhone.endingCallId || state.browserPhone.callPhase === 'ending') { + return 'ending'; + } + if (state.browserPhone.callPhase === 'connecting') { + return 'connecting'; + } + if (state.browserPhone.incoming || state.browserPhone.callPhase === 'incoming') { + return 'incoming'; + } + if (state.browserPhone.callPhase === 'in-call' || state.browserPhone.session) { + return 'in-call'; + } + return 'idle'; +} + +function browserPhoneCallById(callId, options = {}) { + if (!callId) { + return null; + } + const { includeRecent = true } = options; + const live = state.liveCalls.items.find((item) => item.call_id === callId); + if (live) { + return live; + } + if (!includeRecent) { + return null; + } + return state.liveCalls.recentItems.find((item) => item.call_id === callId) || null; +} + +function browserPhoneActivePopupCall() { + const trackedIds = [ + state.browserPhone.endingCallId, + state.browserPhone.popupCallId, + state.browserPhone.autoClaimCallId, + ].filter(Boolean); + for (const callId of trackedIds) { + const found = browserPhoneCallById(callId); + if (found) { + return found; + } + } + const owned = state.liveCalls.items.find((item) => item.claimed_by_user === state.user); + if (owned) { + return owned; + } + const candidates = browserPhoneClaimCandidates(); + if (candidates.length === 1) { + return candidates[0]; + } + const selected = selectedLiveCallItem(); + if (selected) { + return selected; + } + return null; +} + +function browserPhoneCallTimerSource(item) { + return item?.connected_at || item?.started_at || state.browserPhone.sessionStartedAt || ''; +} + +function formatCallTimer(value) { + if (!value) { + return '00:00'; + } + const startedAt = new Date(value); + if (Number.isNaN(startedAt.getTime())) { + return '00:00'; + } + const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt.getTime()) / 1000)); + const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0'); + const seconds = String(totalSeconds % 60).padStart(2, '0'); + return `${minutes}:${seconds}`; +} + +function browserPhonePopupVisible() { + return browserPhonePopupPhase() !== 'idle'; +} + +function browserPhoneCallSummary(item) { + if (!item) { + return 'Ожидаем карточку звонка из bridge...'; + } + const summary = voiceSummaryForItem(item); + const aiMeta = voiceAiStatusMeta(item); + const nameState = voiceCustomerNameStateLine(summary); + const parts = [ + item.queue_code || item.queue_id || 'queue?', + item.interaction_id || 'interaction?', + item.operator_extension ? `ext ${item.operator_extension}` : '', + nameState, + aiMeta ? aiMeta.label : '', + ].filter(Boolean); + return parts.join(' • '); +} + +function browserPhoneHintText(config) { + const popupPhase = browserPhonePopupPhase(); + if (!config) { + return 'Browser softphone недоступен: bridge не вернул конфиг для текущего пользователя.'; + } + if (!config.enabled) { + return 'Browser softphone отключён в окружении. Внешний SIP-клиент остаётся fallback-путём.'; + } + if (!browserPhoneLibraryAvailable()) { + return 'Browser softphone не инициализирован: SIP.js asset не загрузился.'; + } + if (popupPhase === 'incoming' && state.browserPhone.ringtoneActive) { + return 'Входящий звонок в браузере. Используйте Ответить или Отклонить. Рингтон воспроизводится локально.'; + } + if (popupPhase === 'connecting') { + return 'Подключаем медиа и быстро сопоставляем звонок с bridge. При неоднозначности можно нажать «Принять в работу» вручную.'; + } + if (popupPhase === 'ending') { + return 'Ждём финальное подтверждение завершения звонка от bridge.'; + } + if (popupPhase === 'error') { + return 'Локальная media session уже завершена, но bridge ещё не подтвердил финальный terminal state. Проверьте предупреждение в popup.'; + } + if (state.browserPhone.connected) { + return 'Телефон зарегистрирован. Входящий браузерный звонок откроет отдельное окно и попытается выполнить авто-claim.'; + } + return 'Подключите browser phone и разрешите доступ к микрофону.'; +} + +function syncBrowserPhonePopupLifecycle() { + const activeCallId = state.browserPhone.endingCallId || state.browserPhone.popupCallId || state.browserPhone.autoClaimCallId; + if (!activeCallId) { + updateBrowserPhoneUi(); + return; + } + const liveCall = state.liveCalls.items.find((item) => item.call_id === activeCallId); + const recentCall = state.liveCalls.recentItems.find((item) => item.call_id === activeCallId); + if (!liveCall && recentCall && !state.browserPhone.session && !state.browserPhone.incoming) { + state.browserPhone.endingCallId = ''; + state.browserPhone.popupCallId = ''; + state.browserPhone.autoClaimCallId = ''; + state.browserPhone.callPhase = ''; + state.browserPhone.warning = ''; + if (state.browserPhone.connected) { + state.browserPhone.status = 'Зарегистрирован'; + } + } + updateBrowserPhoneUi(); +} + +function updateBrowserPhoneUi() { + const config = browserPhoneConfig(); + const hasSession = Boolean(state.browserPhone.session); + const pendingAction = state.liveCalls.pendingAction || ''; + const popupCall = browserPhoneActivePopupCall(); + const popupPhase = browserPhonePopupPhase(); + const phoneState = browserPhoneStatusState(); + const connectBtn = $('browserPhoneConnectBtn'); + const disconnectBtn = $('browserPhoneDisconnectBtn'); + const micSelect = $('browserPhoneMicSelect'); + const speakerSelect = $('browserPhoneSpeakerSelect'); + const settingsPanel = $('browserPhoneSettingsPanel'); + const statusBtn = $('browserPhoneStatusBtn'); + const statusLabel = $('browserPhoneStatusLabel'); + const hint = $('browserPhoneHint'); + const muteBtn = $('browserPhoneMuteBtn'); + const answerBtn = $('browserPhoneAnswerBtn'); + const rejectBtn = $('browserPhoneRejectBtn'); + const claimBtn = $('browserPhoneCallClaimBtn'); + const editNameBtn = $('browserPhoneEditNameBtn'); + const transferBtn = $('browserPhoneCallTransferBtn'); + const hangupBtn = $('browserPhoneCallHangupBtn'); + const transferType = $('browserPhoneTransferTargetType'); + const transferValue = $('browserPhoneTransferTargetValue'); + const overlay = $('browserPhoneCallOverlay'); + const popupState = $('browserPhoneCallState'); + const popupTitle = $('browserPhoneCallTitle'); + const popupIncomingText = $('browserPhoneIncomingText'); + const popupMeta = $('browserPhoneCallMeta'); + const popupAiSummary = $('browserPhoneAiSummary'); + const popupTimer = $('browserPhoneCallTimer'); + const popupWarning = $('browserPhoneCallWarning'); + const popupAiSummaryData = popupCall ? voiceAiSummaryForCall(popupCall.call_id) : null; + const popupAiSummaryPending = popupCall ? voiceAiSummaryPending(popupCall.call_id) : false; + + $('browserPhoneRegistrationState').textContent = state.browserPhone.status; + $('browserPhoneOperatorExtension').textContent = config?.operator_extension || '—'; + $('browserPhoneWsUrl').textContent = config?.ws_url || '—'; + statusLabel.textContent = browserPhoneStatusLabel(); + statusBtn.dataset.state = phoneState; + statusBtn.setAttribute('aria-expanded', state.browserPhone.settingsOpen ? 'true' : 'false'); + settingsPanel.classList.toggle('hidden', !state.browserPhone.settingsOpen); + + connectBtn.disabled = !config?.enabled || state.browserPhone.connected || state.browserPhone.connecting; + disconnectBtn.disabled = !state.browserPhone.connected && !hasSession && !state.browserPhone.connecting; + micSelect.disabled = state.browserPhone.connecting; + speakerSelect.disabled = state.browserPhone.connecting; + hint.textContent = browserPhoneHintText(config); + + const incoming = popupPhase === 'incoming'; + const connecting = popupPhase === 'connecting'; + const ending = popupPhase === 'ending'; + const errorState = popupPhase === 'error'; + const active = popupPhase === 'in-call'; + const canClaim = Boolean(popupCall && isClaimableLiveCall(popupCall) && !incoming && !connecting && !ending && !state.browserPhone.autoClaimInFlight); + const canControl = Boolean(popupCall && canControlLiveCall(popupCall) && !connecting && !ending); + const popupCustomerId = liveCallCustomerId(popupCall); + + overlay.classList.toggle('hidden', !browserPhonePopupVisible()); + popupState.textContent = errorState + ? 'Нужна синхронизация завершения' + : ending + ? 'Завершаем звонок' + : connecting + ? 'Подключаем media' + : incoming + ? 'Входящий звонок' + : active + ? 'Разговор в браузере' + : 'Browser call'; + popupTitle.textContent = popupCall + ? voiceCustomerDisplayName(popupCall, popupAiSummaryData) + : browserPhoneIncomingLabel(state.browserPhone.session).replace('Входящий звонок: ', ''); + popupIncomingText.textContent = popupCall + ? voiceCustomerIncomingMeta(popupCall, popupAiSummaryData) + : state.browserPhone.session + ? browserPhoneIncomingLabel(state.browserPhone.session) + : 'SIP invite ещё не поступал.'; + popupMeta.textContent = browserPhoneCallSummary(popupCall); + if (popupCall) { + ensureVoiceAiSummaryLoaded(popupCall); + } + popupAiSummary.innerHTML = renderVoiceAiSummary(popupAiSummaryData, popupAiSummaryPending); + popupAiSummary.classList.toggle('hidden', !popupAiSummary.innerHTML.trim()); + popupTimer.textContent = incoming + ? 'ringing' + : connecting + ? 'connecting…' + : ending + ? 'ending…' + : formatCallTimer(browserPhoneCallTimerSource(popupCall)); + popupWarning.textContent = state.browserPhone.warning; + popupWarning.classList.toggle('hidden', !state.browserPhone.warning); + + answerBtn.classList.toggle('hidden', !incoming); + rejectBtn.classList.toggle('hidden', !incoming); + claimBtn.classList.toggle('hidden', !canClaim); + editNameBtn.classList.toggle('hidden', !popupCall || !popupCustomerId); + muteBtn.classList.toggle('hidden', !(active || connecting)); + transferBtn.classList.toggle('hidden', !(canControl || ending || errorState)); + hangupBtn.classList.toggle('hidden', !(popupCall || hasSession || ending || errorState || connecting)); + transferType.classList.toggle('hidden', !(canControl || ending || errorState)); + transferValue.classList.toggle('hidden', !(canControl || ending || errorState)); + + answerBtn.disabled = !incoming || Boolean(pendingAction); + rejectBtn.disabled = !incoming || Boolean(pendingAction); + claimBtn.disabled = !canClaim || Boolean(pendingAction) || state.browserPhone.autoClaimInFlight; + editNameBtn.disabled = !popupCall || !popupCustomerId || state.liveCalls.nameEditor.saving; + muteBtn.disabled = !hasSession; + muteBtn.textContent = state.browserPhone.muted ? 'Включить микрофон' : 'Выключить микрофон'; + transferBtn.disabled = !canControl || Boolean(pendingAction) || ending || connecting || !transferValue.value.trim(); + hangupBtn.disabled = (!popupCall && !hasSession) || Boolean(pendingAction) || ending; + transferType.disabled = !canControl || Boolean(pendingAction) || connecting; + transferValue.disabled = !canControl || Boolean(pendingAction) || connecting; + if (state.liveCalls.nameEditor.mode === 'popup' && (!popupCall || state.liveCalls.nameEditor.callId !== popupCall.call_id)) { + closeLiveCallNameEditor(); + } else { + updateLiveCallNameEditorsUi(); + } +} + +function renderBrowserDeviceOptions(selectId, devices, preferredId, placeholder) { + const select = $(selectId); + if (!select) { + return; + } + const items = devices.length ? devices : [{ deviceId: '', label: placeholder }]; + select.innerHTML = items + .map((item, index) => ``) + .join(''); + if (preferredId && items.some((item) => item.deviceId === preferredId)) { + select.value = preferredId; + } else if (!devices.length) { + select.value = ''; + } +} + +async function loadBrowserPhoneDevices() { + if (!navigator.mediaDevices?.enumerateDevices) { + renderBrowserDeviceOptions('browserPhoneMicSelect', [], '', 'Media devices not available'); + renderBrowserDeviceOptions('browserPhoneSpeakerSelect', [], '', 'Output selection not supported'); + return; + } + const devices = await navigator.mediaDevices.enumerateDevices(); + const inputs = devices.filter((item) => item.kind === 'audioinput'); + const outputs = devices.filter((item) => item.kind === 'audiooutput'); + renderBrowserDeviceOptions('browserPhoneMicSelect', inputs, state.browserPhone.micDeviceId, 'Микрофон'); + renderBrowserDeviceOptions('browserPhoneSpeakerSelect', outputs, state.browserPhone.speakerDeviceId, 'Динамик'); + if (!state.browserPhone.micDeviceId) { + state.browserPhone.micDeviceId = $('browserPhoneMicSelect').value; + } + if (!state.browserPhone.speakerDeviceId) { + state.browserPhone.speakerDeviceId = $('browserPhoneSpeakerSelect').value; + } + syncBrowserPhoneSpeakerSink(); +} + +async function ensureBrowserPhoneMediaAccess() { + if (!navigator.mediaDevices?.getUserMedia) { + throw new Error('Browser getUserMedia is not available'); + } + const stream = await navigator.mediaDevices.getUserMedia(browserPhoneAudioConstraints()); + stream.getTracks().forEach((track) => track.stop()); +} + +function stopBrowserPhoneLocalStream() { + if (state.browserPhone.localStream) { + state.browserPhone.localStream.getTracks().forEach((track) => track.stop()); + } + state.browserPhone.localStream = null; +} + +function browserPhoneLocalStreamHealthy() { + const stream = state.browserPhone.localStream; + return Boolean(stream && stream.getTracks().some((track) => track.readyState === 'live')); +} + +async function ensureBrowserPhoneLocalStream(options = {}) { + const { forceFresh = false } = options; + if (!navigator.mediaDevices?.getUserMedia) { + throw new Error('Browser getUserMedia is not available'); + } + if (forceFresh) { + stopBrowserPhoneLocalStream(); + } + if (browserPhoneLocalStreamHealthy()) { + return state.browserPhone.localStream; + } + if (state.browserPhone.localStreamPromise) { + return state.browserPhone.localStreamPromise; + } + state.browserPhone.localStreamPromise = navigator.mediaDevices + .getUserMedia(browserPhoneAudioConstraints()) + .then((stream) => { + stopBrowserPhoneLocalStream(); + state.browserPhone.localStream = stream; + return stream; + }) + .finally(() => { + state.browserPhone.localStreamPromise = null; + }); + return state.browserPhone.localStreamPromise; +} + +function prepareBrowserPhoneAnswerMedia() { + return ensureBrowserPhoneLocalStream().catch((err) => { + log('Browser softphone не смог заранее подготовить media', { error: err.message }); + return null; + }); +} + +function browserPhoneClaimCandidates() { + return browserPhoneClaimCandidatesFrom(state.liveCalls.items); +} + +function browserPhoneCallTimestampMs(item) { + const raw = item?.last_transition_at || item?.connected_at || item?.started_at || item?.updated_at || ''; + const ts = raw ? new Date(raw).getTime() : Number.NaN; + return Number.isFinite(ts) ? ts : Number.NaN; +} + +function browserPhoneSessionTimestampMs() { + const raw = state.browserPhone.sessionStartedAt || ''; + const ts = raw ? new Date(raw).getTime() : Number.NaN; + return Number.isFinite(ts) ? ts : Number.NaN; +} + +function browserPhoneClaimCandidatesFrom(items) { + const config = browserPhoneConfig(); + const extension = config?.operator_extension || ''; + return (items || []).filter((item) => { + if (!['ringing', 'claimed', 'connected'].includes(item.telephony_status)) { + return false; + } + if (extension && item.operator_extension && item.operator_extension !== extension) { + return false; + } + return true; + }); +} + +function browserPhoneBestCandidateMatch(candidates) { + if (!Array.isArray(candidates) || !candidates.length) { + return null; + } + const sessionTs = browserPhoneSessionTimestampMs(); + const ranked = [...candidates].sort((left, right) => { + const leftTs = browserPhoneCallTimestampMs(left); + const rightTs = browserPhoneCallTimestampMs(right); + if (Number.isFinite(sessionTs)) { + const leftDelta = Number.isFinite(leftTs) ? Math.abs(leftTs - sessionTs) : Number.POSITIVE_INFINITY; + const rightDelta = Number.isFinite(rightTs) ? Math.abs(rightTs - sessionTs) : Number.POSITIVE_INFINITY; + if (leftDelta !== rightDelta) { + return leftDelta - rightDelta; + } + } + if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) { + return rightTs - leftTs; + } + return String(right?.call_id || '').localeCompare(String(left?.call_id || '')); + }); + return ranked[0] || null; +} + +function attachBrowserRemoteAudio(session) { + const element = browserPhoneAudioElement(); + const sdh = session?.sessionDescriptionHandler; + const peerConnection = sdh?.peerConnection; + if (!element || !peerConnection) { + return; + } + + const syncRemoteStream = () => { + const stream = new MediaStream(); + peerConnection.getReceivers().forEach((receiver) => { + if (receiver.track) { + stream.addTrack(receiver.track); + } + }); + element.srcObject = stream; + syncBrowserPhoneSpeakerSink(); + element.play().catch(() => {}); + }; + + syncRemoteStream(); + peerConnection.addEventListener('track', syncRemoteStream); +} + +function applyBrowserMuteState() { + const sdh = state.browserPhone.session?.sessionDescriptionHandler; + const peerConnection = sdh?.peerConnection; + if (!peerConnection) { + return; + } + peerConnection.getSenders().forEach((sender) => { + if (sender.track && sender.track.kind === 'audio') { + sender.track.enabled = !state.browserPhone.muted; + } + }); +} + +function resetBrowserPhoneSessionState() { + stopBrowserPhoneRingtone(); + state.browserPhone.session = null; + state.browserPhone.incoming = false; + state.browserPhone.muted = false; + state.browserPhone.autoClaimCallId = ''; + state.browserPhone.autoClaimInFlight = false; + state.browserPhone.sessionStartedAt = ''; + state.browserPhone.callPhase = state.browserPhone.endingCallId ? 'ending' : ''; + stopBrowserPhoneLocalStream(); + const element = browserPhoneAudioElement(); + if (element) { + element.srcObject = null; + } + updateBrowserPhoneUi(); +} + +function browserSessionStateValue(session = state.browserPhone.session) { + const SessionState = window.SIP?.SessionState; + if (!session || !SessionState) { + return null; + } + return session.state; +} + +function browserSessionIsActive(session = state.browserPhone.session) { + const SessionState = window.SIP?.SessionState; + if (!session || !SessionState) { + return false; + } + return ![SessionState.Terminated].includes(session.state); +} + +function ensureBrowserSessionFreshForInvite() { + if (!state.browserPhone.session) { + return true; + } + if (browserSessionIsActive(state.browserPhone.session)) { + return false; + } + resetBrowserPhoneSessionState(); + return true; +} + +function bindBrowserPhoneSession(session) { + const SessionState = window.SIP?.SessionState; + state.browserPhone.session = session; + state.browserPhone.incoming = true; + state.browserPhone.muted = false; + state.browserPhone.popupCallId = ''; + state.browserPhone.callPhase = 'incoming'; + state.browserPhone.autoClaimInFlight = false; + state.browserPhone.warning = ''; + setBrowserPhoneStatus('Входящий звонок'); + startBrowserPhoneRingtone().catch(() => {}); + prepareBrowserPhoneAnswerMedia(); + updateBrowserPhoneUi(); + session.stateChange.addListener((newState) => { + if (newState === SessionState.Established) { + stopBrowserPhoneRingtone(); + state.browserPhone.incoming = false; + state.browserPhone.callPhase = 'in-call'; + state.browserPhone.sessionStartedAt = new Date().toISOString(); + setBrowserPhoneStatus('Соединено в браузере'); + attachBrowserRemoteAudio(session); + applyBrowserMuteState(); + if (!state.browserPhone.autoClaimInFlight && !state.browserPhone.popupCallId && !state.browserPhone.autoClaimCallId) { + void bestEffortBrowserAutoClaim({ refreshAttempts: 1, retryDelayMs: 150 }); + } + return; + } + if (newState === SessionState.Terminated) { + resetBrowserPhoneSessionState(); + if (state.browserPhone.connected && !state.browserPhone.endingCallId) { + setBrowserPhoneStatus('Зарегистрирован'); + } + syncBrowserPhonePopupLifecycle(); + } + }); +} + +async function terminateBrowserPhoneSessionLocally() { + const session = state.browserPhone.session; + if (!session) { + return; + } + try { + const SessionState = window.SIP?.SessionState; + if (state.browserPhone.incoming && typeof session.reject === 'function') { + await session.reject(); + return; + } + if (session.state === SessionState.Established && typeof session.bye === 'function') { + await session.bye(); + return; + } + if (typeof session.cancel === 'function') { + await session.cancel(); + return; + } + if (typeof session.dispose === 'function') { + session.dispose(); + } + } catch { + if (typeof session.dispose === 'function') { + session.dispose(); + } + } +} + +async function claimLiveCallById(callId, options = {}) { + const operatorExtension = (options.operatorExtension || browserPhoneConfig()?.operator_extension || '').trim(); + const payload = {}; + if (operatorExtension) { + payload.operator_extension = operatorExtension; + } + const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/claim`, { + method: 'POST', + body: JSON.stringify(payload), + }); + state.browserPhone.popupCallId = data.call_id; + state.browserPhone.endingCallId = ''; + state.browserPhone.warning = ''; + applyClaimedLiveCall(data); + refreshLiveCallsInBackground(); + return data; +} + +async function hangupLiveCallById(callId, options = {}) { + const { optimistic = true } = options; + const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/hangup`, { + method: 'POST', + body: JSON.stringify({}), + }); + if (optimistic) { + moveLiveCallToRecent(data); + refreshLiveCallsInBackground(); + } else { + state.browserPhone.endingCallId = callId; + updateBrowserPhoneUi(); + refreshLiveCallsInBackground(); + } + return data; +} + +async function bestEffortBrowserAutoClaim(options = {}) { + return bestEffortBrowserAutoClaimWithOptions(options); +} + +function browserPhoneAutoClaimSnapshot(items = state.liveCalls.items) { + const byId = (callId) => { + if (!callId) { + return null; + } + return (items || []).find((item) => item.call_id === callId) || null; + }; + + const trackedIds = [ + state.browserPhone.popupCallId, + state.browserPhone.autoClaimCallId, + selectedLiveCallId(), + ].filter(Boolean); + + for (const callId of trackedIds) { + const found = byId(callId); + if (!found) { + continue; + } + if (found.claimed_by_user === state.user) { + return { mode: 'owned', call: found }; + } + if (isClaimableLiveCall(found)) { + return { mode: 'claim', call: found }; + } + } + + const candidates = browserPhoneClaimCandidatesFrom(items); + const ownedCandidates = candidates.filter((item) => item.claimed_by_user === state.user); + if (ownedCandidates.length === 1) { + return { mode: 'owned', call: ownedCandidates[0] }; + } + if (ownedCandidates.length > 1) { + const bestOwned = browserPhoneBestCandidateMatch(ownedCandidates); + if (bestOwned) { + return { mode: 'owned', call: bestOwned }; + } + } + const unclaimedCandidates = candidates.filter((item) => !item.claimed_by_user); + if (unclaimedCandidates.length === 1) { + return { mode: 'claim', call: unclaimedCandidates[0] }; + } + if (unclaimedCandidates.length > 1) { + const bestUnclaimed = browserPhoneBestCandidateMatch(unclaimedCandidates); + if (bestUnclaimed) { + return { mode: 'claim', call: bestUnclaimed }; + } + } + return null; +} + +async function bestEffortBrowserAutoClaimWithOptions(options = {}) { + const { refreshAttempts = 2, retryDelayMs = 150 } = options; + if (state.browserPhone.autoClaimInFlight) { + return false; + } + state.browserPhone.autoClaimInFlight = true; + updateBrowserPhoneUi(); + const config = browserPhoneConfig(); + const operatorExtension = config?.operator_extension || ''; + const handleMatch = async (match) => { + if (!match?.call) { + return false; + } + if (match.mode === 'owned') { + state.browserPhone.autoClaimCallId = match.call.call_id; + state.browserPhone.popupCallId = match.call.call_id; + state.browserPhone.warning = ''; + return true; + } + if (match.mode === 'claim') { + try { + const claimed = await claimLiveCallById(match.call.call_id, { operatorExtension }); + state.browserPhone.autoClaimCallId = claimed.call_id; + state.browserPhone.popupCallId = claimed.call_id; + state.browserPhone.warning = ''; + log('Browser softphone выполнил auto-claim', { call_id: claimed.call_id, extension: operatorExtension || claimed.operator_extension }); + return true; + } catch (err) { + setBrowserPhoneWarning('Звонок соединён, но авто-claim не выполнился. Нажмите «Принять в работу».'); + log('Browser softphone не смог выполнить auto-claim', { error: describeLiveCallError(err.message) }); + return false; + } + } + return false; + }; + + try { + const localMatch = browserPhoneAutoClaimSnapshot(); + if (await handleMatch(localMatch)) { + return true; + } + for (let attempt = 0; attempt < refreshAttempts; attempt += 1) { + if (attempt > 0 && retryDelayMs > 0) { + await new Promise((resolve) => window.setTimeout(resolve, retryDelayMs)); + } + await loadLiveCalls(false, { preserveStateOnError: true }); + const refreshedMatch = browserPhoneAutoClaimSnapshot(); + if (await handleMatch(refreshedMatch)) { + return true; + } + } + setBrowserPhoneWarning('Не удалось однозначно сопоставить звонок. Нажмите «Принять в работу» вручную.'); + log('Browser softphone требует ручного claim', { candidates: browserPhoneClaimCandidates().length }); + return false; + } finally { + state.browserPhone.autoClaimInFlight = false; + updateBrowserPhoneUi(); + } +} + +async function fetchBrowserSoftphoneConfig() { + try { + const data = await api('asterisk-bridge', 'asterisk/browser-softphone/config'); + state.browserPhone.config = data; + state.browserPhone.status = data.enabled ? 'Готов к подключению' : 'Отключён в окружении'; + } catch (err) { + state.browserPhone.config = null; + state.browserPhone.status = 'Не настроен'; + log('Browser softphone config недоступен', { error: describeLiveCallError(err.message) }); + } finally { + updateBrowserPhoneUi(); + } +} + +async function connectBrowserSoftphone() { + if (state.browserPhone.connecting || state.browserPhone.connected) { + return; + } + if (!browserPhoneLibraryAvailable()) { + setBrowserPhoneStatus('SIP.js asset не загружен'); + return; + } + if (!browserPhoneConfig()) { + await fetchBrowserSoftphoneConfig(); + } + const config = browserPhoneConfig(); + if (!config?.enabled) { + updateBrowserPhoneUi(); + return; + } + + try { + state.browserPhone.connecting = true; + state.browserPhone.settingsOpen = false; + state.browserPhone.warning = ''; + setBrowserPhoneStatus('Подключаем browser softphone...'); + await ensureBrowserPhoneMediaAccess(); + await ensureBrowserPhoneAudioContextResumed(); + await loadBrowserPhoneDevices(); + + const uri = window.SIP.UserAgent.makeURI(config.sip_uri); + if (!uri) { + throw new Error('Invalid browser SIP URI'); + } + + const userAgent = new window.SIP.UserAgent({ + uri, + authorizationUsername: config.authorization_username, + authorizationPassword: config.password, + displayName: config.display_name, + transportOptions: { server: config.ws_url }, + sessionDescriptionHandlerFactoryOptions: { + constraints: browserPhoneAudioConstraints(), + peerConnectionConfiguration: { + iceServers: Array.isArray(config.ice_servers) ? config.ice_servers : [], + iceCandidatePoolSize: 4, + bundlePolicy: 'max-bundle', + rtcpMuxPolicy: 'require', + }, + }, + }); + userAgent.delegate = { + onInvite: (invitation) => { + if (!ensureBrowserSessionFreshForInvite()) { + invitation.reject().catch(() => {}); + log('Browser softphone отклонил входящий INVITE: предыдущая session ещё активна'); + return; + } + bindBrowserPhoneSession(invitation); + log('Browser softphone получил входящий звонок', { remote: browserPhoneIncomingLabel(invitation) }); + }, + }; + const registerer = new window.SIP.Registerer(userAgent); + await userAgent.start(); + await registerer.register(); + + state.browserPhone.ua = userAgent; + state.browserPhone.registerer = registerer; + state.browserPhone.connected = true; + setBrowserPhoneStatus('Зарегистрирован'); + } catch (err) { + state.browserPhone.connected = false; + state.browserPhone.ua = null; + state.browserPhone.registerer = null; + setBrowserPhoneStatus('Ошибка регистрации'); + log('Browser softphone не подключился', { error: err.message }); + } finally { + state.browserPhone.connecting = false; + updateBrowserPhoneUi(); + } +} + +async function disconnectBrowserSoftphone() { + try { + stopBrowserPhoneRingtone(); + if (state.browserPhone.session) { + await terminateBrowserPhoneSessionLocally(); + } + if (state.browserPhone.registerer) { + await state.browserPhone.registerer.unregister().catch(() => {}); + } + if (state.browserPhone.ua) { + await state.browserPhone.ua.stop().catch(() => {}); + } + } finally { + state.browserPhone.connected = false; + state.browserPhone.connecting = false; + state.browserPhone.settingsOpen = false; + state.browserPhone.ua = null; + state.browserPhone.registerer = null; + state.browserPhone.popupCallId = ''; + state.browserPhone.endingCallId = ''; + state.browserPhone.callPhase = ''; + state.browserPhone.warning = ''; + stopBrowserPhoneLocalStream(); + resetBrowserPhoneSessionState(); + setBrowserPhoneStatus(browserPhoneConfig()?.enabled ? 'Отключён' : 'Не настроен'); + } +} + +async function answerBrowserSoftphoneCall() { + const session = state.browserPhone.session; + if (!session || !state.browserPhone.incoming) { + return; + } + try { + stopBrowserPhoneRingtone(); + state.browserPhone.warning = ''; + state.browserPhone.incoming = false; + state.browserPhone.callPhase = 'connecting'; + setBrowserPhoneStatus('Соединяем browser media...'); + updateBrowserPhoneUi(); + const localMediaStream = await ensureBrowserPhoneLocalStream(); + await session.accept({ + sessionDescriptionHandlerOptions: { + constraints: browserPhoneAudioConstraints(), + localMediaStream, + }, + }); + const hasImmediateMatch = Boolean(browserPhoneAutoClaimSnapshot()); + void bestEffortBrowserAutoClaimWithOptions({ + refreshAttempts: hasImmediateMatch ? 0 : 2, + retryDelayMs: 150, + }); + } catch (err) { + state.browserPhone.incoming = true; + state.browserPhone.callPhase = 'incoming'; + startBrowserPhoneRingtone().catch(() => {}); + setBrowserPhoneWarning('Не удалось ответить на звонок в браузере.'); + setBrowserPhoneStatus('Входящий звонок'); + log('Ответ в браузерном телефоне не выполнен', { error: err.message }); + } +} + +async function rejectBrowserSoftphoneCall() { + if (!state.browserPhone.session || !state.browserPhone.incoming) { + return; + } + stopBrowserPhoneRingtone(); + state.browserPhone.warning = ''; + await terminateBrowserPhoneSessionLocally(); + resetBrowserPhoneSessionState(); + state.browserPhone.callPhase = ''; + if (state.browserPhone.connected) { + setBrowserPhoneStatus('Зарегистрирован'); + } +} + +function toggleBrowserSoftphoneMute() { + if (!state.browserPhone.session) { + return; + } + state.browserPhone.muted = !state.browserPhone.muted; + applyBrowserMuteState(); + updateBrowserPhoneUi(); +} + +function syncLiveCallActionButtons() { + const loadActionsBtn = $('loadLiveCallActionsBtn'); + const refreshBtn = $('loadLiveCallsBtn'); + const buttons = [loadActionsBtn, refreshBtn].filter(Boolean); + const hasCall = Boolean(selectedLiveCallId()); + const pendingAction = state.liveCalls.pendingAction || ''; + + buttons.forEach((button) => { + if (!button.dataset.defaultLabel) { + button.dataset.defaultLabel = button.textContent; + } + button.textContent = button.dataset.defaultLabel; + }); + + loadActionsBtn.disabled = !hasCall || Boolean(pendingAction); + refreshBtn.disabled = Boolean(pendingAction); + updateBrowserPhoneUi(); +} + +async function runLiveCallAction(actionName, handler) { + if (state.liveCalls.pendingAction) { + log('Дождитесь завершения предыдущего действия по звонку'); + return null; + } + state.liveCalls.pendingAction = actionName; + syncLiveCallActionButtons(); + try { + return await handler(); + } finally { + state.liveCalls.pendingAction = ''; + syncLiveCallActionButtons(); + } +} + +async function claimBrowserPopupCall() { + const popupCall = browserPhoneActivePopupCall(); + if (!popupCall) { + setBrowserPhoneWarning('Не удалось найти активный звонок для claim.'); + return; + } + await runLiveCallAction('claim', async () => { + try { + const data = await claimLiveCallById(popupCall.call_id); + state.browserPhone.popupCallId = data.call_id; + state.browserPhone.warning = ''; + log('Звонок принят в работу', { + call_id: data.call_id, + claimed_by: data.claimed_by_user, + extension: data.operator_extension, + }); + } catch (err) { + setBrowserPhoneWarning(describeLiveCallError(err.message)); + log('Не удалось принять звонок в работу', { call_id: popupCall.call_id, error: describeLiveCallError(err.message) }); + } + }); +} + +async function hangupBrowserPopupCall() { + const popupCall = browserPhoneActivePopupCall(); + if (!popupCall && !state.browserPhone.session) { + setBrowserPhoneWarning('Не удалось найти активный звонок для завершения.'); + return; + } + await runLiveCallAction('hangup', async () => { + const callId = popupCall?.call_id || state.browserPhone.popupCallId || state.browserPhone.autoClaimCallId || selectedLiveCallId(); + try { + state.browserPhone.endingCallId = callId; + state.browserPhone.callPhase = 'ending'; + state.browserPhone.warning = ''; + updateBrowserPhoneUi(); + if (state.browserPhone.session) { + await terminateBrowserPhoneSessionLocally(); + resetBrowserPhoneSessionState(); + state.browserPhone.endingCallId = callId; + state.browserPhone.popupCallId = callId; + state.browserPhone.callPhase = 'ending'; + updateBrowserPhoneUi(); + } + if (!callId) { + throw new Error('hangup failed: call_id is missing'); + } + await hangupLiveCallById(callId, { optimistic: false }); + log('Отправлена команда завершения звонка', { call_id: callId }); + } catch (err) { + state.browserPhone.callPhase = 'error'; + state.browserPhone.endingCallId = callId || state.browserPhone.endingCallId; + setBrowserPhoneWarning(describeLiveCallError(err.message)); + refreshLiveCallsInBackground(); + log('Не удалось завершить звонок', { call_id: callId || popupCall?.call_id || '', error: describeLiveCallError(err.message) }); + } + }); +} + +async function transferBrowserPopupCall() { + const popupCall = browserPhoneActivePopupCall(); + const targetType = $('browserPhoneTransferTargetType').value; + const targetValue = $('browserPhoneTransferTargetValue').value.trim(); + if (!popupCall) { + setBrowserPhoneWarning('Не удалось найти активный звонок для передачи.'); + return; + } + if (!targetValue) { + setBrowserPhoneWarning('Укажите target для передачи звонка.'); + return; + } + await runLiveCallAction('transfer', async () => { + try { + const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(popupCall.call_id)}/blind-transfer`, { + method: 'POST', + body: JSON.stringify({ target_type: targetType, target_value: targetValue }), + }); + if (state.browserPhone.session) { + await terminateBrowserPhoneSessionLocally(); + resetBrowserPhoneSessionState(); + } + state.browserPhone.popupCallId = ''; + state.browserPhone.endingCallId = ''; + state.browserPhone.warning = ''; + moveLiveCallToRecent(data); + refreshLiveCallsInBackground(); + log('Blind transfer выполнен', { + call_id: data.call_id, + target_type: targetType, + target_value: targetValue, + }); + } catch (err) { + setBrowserPhoneWarning(describeLiveCallError(err.message)); + log('Не удалось передать звонок', { call_id: popupCall.call_id, error: describeLiveCallError(err.message) }); + } + }); +} + +function updateLiveCallSelector(items) { + const select = $('liveCallIdSelect'); + const previous = state.liveCalls.selectedCallId || select.value || ''; + const sorted = [...items]; + select.innerHTML = sorted.length + ? sorted + .map((item) => { + const summary = voiceSummaryForItem(item); + const label = `${voiceCustomerDisplayName(item, summary)} | ${item.call_id}${item.interaction_id ? ` | ${item.interaction_id}` : ''}`; + return ``; + }) + .join('') + : ''; + if (sorted.length) { + const hasPrevious = sorted.some((item) => item.call_id === previous); + const latestRinging = sorted.find((item) => item.telephony_status === 'ringing' && !item.claimed_by_user); + if (latestRinging && latestRinging.call_id !== previous) { + select.value = latestRinging.call_id; + } else { + select.value = hasPrevious ? previous : sorted[0].call_id; + } + } else { + select.value = ''; + } + state.liveCalls.selectedCallId = select.value; + syncLiveCallActionButtons(); +} + +function renderLiveCallCard(item, { recent = false } = {}) { + const isSelected = item.call_id === state.liveCalls.selectedCallId; + const summary = voiceSummaryForItem(item); + const caller = voiceCustomerDisplayName(item, summary); + const aiMeta = voiceAiStatusMeta(item); + const customerId = liveCallCustomerId(item); + const nameStatusMeta = voiceCustomerNameStatusMeta(summary?.customer_name_status); + const nameStatusLine = nameStatusMeta + ? `

            имя: ${escapeHtml(nameStatusMeta.label)}

            ` + : ''; + const nameSourceLabel = formatVoiceCustomerNameSource(summary?.customer_name_source); + const nameSourceLine = nameSourceLabel + ? `

            источник имени: ${escapeHtml(nameSourceLabel)}

            ` + : ''; + const languageLabel = formatVoiceStartLanguage(summary?.voice_start_language); + const languageLine = languageLabel + ? `

            язык старта: ${escapeHtml(languageLabel)}

            ` + : ''; + const badges = [ + `${escapeHtml(item.queue_code || item.queue_id)}`, + `${escapeHtml(telephonyLabel(item.telephony_status))}`, + ]; + if (aiMeta) { + badges.push(`${escapeHtml(aiMeta.label)}`); + } + if (nameStatusMeta) { + badges.push(`${escapeHtml(nameStatusMeta.shortLabel)}`); + } + if (recent && item.terminal_action) { + badges.push(`${escapeHtml(terminalActionLabel(item))}`); + } + if (!recent && item.claimed_by_user) { + badges.push(`${escapeHtml(item.claimed_by_user)}`); + } + const targetLine = recent && item.terminal_action === 'blind-transfer' && item.terminal_target + ? `

            цель: ${escapeHtml(item.terminal_target)}

            ` + : ''; + const hangupLine = recent && item.hangup_cause + ? `

            завершение: ${escapeHtml(item.hangup_cause)}

            ` + : ''; + const aiReasonLine = item.ai_handoff_reason + ? `

            AI: ${escapeHtml(item.ai_handoff_reason)}

            ` + : ''; + const timingLine = recent + ? `

            завершён: ${escapeHtml(formatIsoShort(item.last_transition_at || item.ended_at || item.updated_at))}

            ` + : `

            соединён: ${escapeHtml(formatIsoShort(item.connected_at || item.started_at))}

            `; + const actions = [ + customerId ? `` : '', + customerId ? `` : '', + ].filter(Boolean); + return ` +
            +
            ${badges.join('')}
            +

            ${escapeHtml(caller)}

            +

            ${escapeHtml(voiceCustomerCallSubtitle(item))}

            +

            обращение: ${escapeHtml(item.interaction_id || 'не найдено')}

            +

            взял в работу: ${escapeHtml(item.claimed_by_user || '-')}

            +

            внутренний номер: ${escapeHtml(item.operator_extension || '-')}

            +

            начат: ${escapeHtml(formatIsoShort(item.started_at))}

            + ${timingLine} + ${nameStatusLine} + ${nameSourceLine} + ${languageLine} + ${aiReasonLine} + ${targetLine} + ${hangupLine} +

            запись: ${item.has_recording ? 'да' : 'нет'}

            + ${actions.length ? `
            ${actions.join('')}
            ` : ''} +
            + `; +} + +function renderLiveCallColumn(title, items, emptyMessage, options = {}) { + const { recent = false } = options; + return ` +
            +
            +
            ${escapeHtml(title)}
            +
            ${items.length}
            +
            +
            + ${items.length ? items.map((item) => renderLiveCallCard(item, { recent })).join('') : `
            ${escapeHtml(emptyMessage)}
            `} +
            +
            + `; +} + +function renderLiveCallsTable(activeItems, recentItems) { + activeItems.forEach((item) => ensureVoiceAiSummaryLoaded(item)); + recentItems.forEach((item) => ensureVoiceAiSummaryLoaded(item)); + $('liveCallsTable').innerHTML = ` +
            + ${renderLiveCallColumn('Активные звонки', activeItems, 'Активных звонков нет.')} + ${renderLiveCallColumn('Только что завершённые', recentItems, 'Недавних завершённых звонков нет.', { recent: true })} +
            + `; + + if (!activeItems.length && !recentItems.length) { + $('liveCallStatusHint').textContent = 'Активных звонков нет.'; + syncLiveCallActionButtons(); + return; + } + + if (activeItems.length) { + const first = activeItems[0]; + $('liveCallStatusHint').textContent = `Активных: ${activeItems.length}. Недавно завершённых: ${recentItems.length}. Последний активный: ${first.call_id} (${telephonyLabel(first.telephony_status)}).`; + } else { + const firstRecent = recentItems[0]; + $('liveCallStatusHint').textContent = `Активных нет. Недавно завершённых: ${recentItems.length}. Последний завершённый: ${firstRecent.call_id} (${terminalActionLabel(firstRecent)}).`; + } + syncLiveCallActionButtons(); +} + +function uniqueLiveCalls(items) { + const seen = new Set(); + return (items || []).filter((item) => { + const callId = item?.call_id; + if (!callId || seen.has(callId)) { + return false; + } + seen.add(callId); + return true; + }); +} + +function applyLiveCallCollections(activeItems, recentItems, options = {}) { + const { selectedCallId = undefined } = options; + state.liveCalls.items = uniqueLiveCalls(activeItems); + state.liveCalls.recentItems = uniqueLiveCalls(recentItems); + if (selectedCallId !== undefined) { + state.liveCalls.selectedCallId = selectedCallId; + } + updateLiveCallSelector(state.liveCalls.items); + renderLiveCallsTable(state.liveCalls.items, state.liveCalls.recentItems); + updateLiveCallNameEditorsUi(); + syncBrowserPhonePopupLifecycle(); + renderCustomerSpotlight(); + renderUnifiedInbox(); +} + +function applyClaimedLiveCall(item) { + const activeItems = [...state.liveCalls.items]; + const existingIndex = activeItems.findIndex((entry) => entry.call_id === item.call_id); + if (existingIndex >= 0) { + activeItems[existingIndex] = item; + } else { + activeItems.unshift(item); + } + const recentItems = state.liveCalls.recentItems.filter((entry) => entry.call_id !== item.call_id); + applyLiveCallCollections(activeItems, recentItems, { selectedCallId: item.call_id }); +} + +function moveLiveCallToRecent(item) { + const activeItems = state.liveCalls.items.filter((entry) => entry.call_id !== item.call_id); + const recentItems = [item, ...state.liveCalls.recentItems.filter((entry) => entry.call_id !== item.call_id)]; + const nextSelectedCallId = state.liveCalls.selectedCallId === item.call_id + ? (activeItems[0]?.call_id || '') + : state.liveCalls.selectedCallId; + applyLiveCallCollections(activeItems, recentItems, { selectedCallId: nextSelectedCallId }); +} + +function refreshLiveCallsInBackground() { + window.setTimeout(() => { + loadLiveCalls(false, { preserveStateOnError: true }); + }, 0); +} + +async function loadLiveCalls(logResult = true, options = {}) { + const { preserveStateOnError = false } = options; + try { + const [activeData, recentData] = await Promise.all([ + api('asterisk-bridge', 'asterisk/live-calls'), + api('asterisk-bridge', 'asterisk/recent-calls'), + ]); + const items = Array.isArray(activeData) ? activeData : []; + const recentItems = Array.isArray(recentData) ? recentData : []; + applyLiveCallCollections(items, recentItems); + if (logResult) { + log('Живые звонки обновлены', { active: items.length, recent: recentItems.length }); + } + } catch (err) { + if (!preserveStateOnError) { + applyLiveCallCollections([], [], { selectedCallId: '' }); + $('liveCallStatusHint').textContent = 'Сервис live-calls недоступен.'; + } + if (logResult) { + log('Не удалось загрузить звонки', { error: describeLiveCallError(err.message) }); + } + } finally { + syncLiveCallActionButtons(); + } +} + +async function claimLiveCall() { + await claimBrowserPopupCall(); +} + +async function hangupLiveCall() { + await hangupBrowserPopupCall(); +} + +async function blindTransferLiveCall() { + await transferBrowserPopupCall(); +} + +async function loadLiveCallActions() { + const callId = selectedLiveCallId(); + if (!callId) { + $('liveCallActionsOutput').textContent = 'Выберите звонок.'; + return; + } + try { + const data = await api('asterisk-bridge', `asterisk/live-calls/${encodeURIComponent(callId)}/actions`); + setOutput('liveCallActionsOutput', data); + log('Загружен action log по звонку', { call_id: callId, actions: data.length }); + } catch (err) { + $('liveCallActionsOutput').textContent = err.message; + log('Не удалось загрузить action log', { call_id: callId, error: err.message }); + } +} + +function startLiveCallsPolling() { + if (state.liveCalls.pollTimer) { + window.clearInterval(state.liveCalls.pollTimer); + state.liveCalls.pollTimer = null; + } + state.liveCalls.pollTimer = window.setInterval(() => { + loadLiveCalls(false); + }, 3000); +} + +async function loadInteractions() { + try { + const data = await api('interaction', 'interactions'); + state.interactions = Array.isArray(data) ? data : []; + if (!data.length) { + setEmptyBlock('interactionTable', 'Обращений пока нет. После подготовки демо они появятся автоматически.'); + renderCustomerSpotlight(); + renderUnifiedInbox(); + return; + } + $('interactionTable').innerHTML = renderInteractionBoard(data); + renderCustomerSpotlight(); + renderUnifiedInbox(); + } catch (err) { + state.interactions = []; + setEmptyBlock('interactionTable', 'Не удалось загрузить обращения.'); + renderCustomerSpotlight(); + renderUnifiedInbox(); + log('Не удалось загрузить обращения', { error: err.message }); + } +} + +window.assignInteraction = async function assignInteraction(id) { + const assignee = getDefaultAssignee(); + try { + await api('interaction', `interactions/${id}/assign`, { + method: 'PATCH', + body: JSON.stringify({ assignee }), + }); + await loadInteractions(); + log('Обращение назначено', { id, assignee }); + } catch (err) { + log('Не удалось назначить обращение', { error: err.message }); + } +}; + +window.escalateInteraction = async function escalateInteraction(id) { + const targetQueueId = getDefaultEscalationQueue(); + try { + await api('interaction', `interactions/${id}/escalate`, { + method: 'POST', + body: JSON.stringify({ target_queue_id: targetQueueId }), + }); + await loadInteractions(); + log('Обращение передано на 2 линию', { id, target_queue_id: targetQueueId }); + } catch (err) { + log('Не удалось передать обращение', { error: err.message }); + } +}; + +window.closeInteraction = async function closeInteraction(id) { + try { + await api('interaction', `interactions/${id}/status`, { + method: 'PATCH', + body: JSON.stringify({ status: 'closed' }), + }); + await loadInteractions(); + log('Обращение закрыто', { id }); + } catch (err) { + log('Не удалось закрыть обращение', { error: err.message }); + } +}; + +async function loadSupervisor() { + try { + const data = await api('supervisor', 'supervisor/realtime'); + renderSupervisorSummary(data); + setOutput('supervisorOutput', data); + log('Данные супервизора обновлены', { + agents: data?.agents?.total || 0, + queue: data?.queues?.[0]?.queue_id || 'нет', + }); + } catch (err) { + $('supervisorSummary').innerHTML = renderSummaryCard('Супервизор', 'нет данных', 'Сервис не ответил'); + $('supervisorOutput').textContent = err.message; + log('Не удалось обновить супервизора', { error: err.message }); + } +} + +async function loadKpi() { + try { + const data = await api('reporting', 'reports/kpi'); + renderKpiSummary(data); + setOutput('kpiOutput', data); + log('KPI обновлены', data?.kpi || {}); + } catch (err) { + $('kpiSummary').innerHTML = renderSummaryCard('KPI', 'нет данных', 'Сервис не ответил'); + $('kpiOutput').textContent = err.message; + log('Не удалось обновить KPI', { error: err.message }); + } +} + +async function handleCustomerActionClick(event) { + const button = event.target.closest('[data-customer-action]'); + if (!button) { + return; + } + const customer = selectedCustomer(); + if (!customer) { + return; + } + if (button.dataset.customerAction === 'edit-name') { + openCustomerProfileNameEditor(customer.customer_id); + return; + } + if (button.dataset.customerAction === 'cancel-name-edit') { + closeCustomerProfileNameEditor(); + return; + } + if (button.dataset.customerAction === 'workspace') { + $('interactionCustomerId').value = customer.customer_id; + window.location.hash = '#workspace'; + log('Клиент открыт в рабочем столе', { customer_id: customer.customer_id }); + return; + } + if (button.dataset.customerAction === 'telegram') { + const threadId = button.dataset.threadId || ''; + if (!threadId) { + return; + } + window.location.hash = '#messages'; + await selectMessengerConversation(messengerConversationId('telegram', threadId)); + log('Открыт Telegram клиента в Сообщениях', { customer_id: customer.customer_id, thread_id: threadId }); + return; + } + if (button.dataset.customerAction === 'interaction') { + const interactionId = button.dataset.interactionId || ''; + $('interactionCustomerId').value = customer.customer_id; + window.location.hash = '#workspace'; + log('Открыт кейс клиента', { customer_id: customer.customer_id, interaction_id: interactionId || '-' }); + return; + } + if (button.dataset.customerAction === 'call') { + const callId = button.dataset.callId || ''; + if (!callId) { + return; + } + focusLiveCall(callId); + window.location.hash = '#calls'; + log('Открыт звонок клиента', { customer_id: customer.customer_id, call_id: callId }); + } +} + +function handleCustomerProfileNameInput(event) { + const input = event.target?.closest?.('[data-customer-name-input]'); + if (!input) { + return; + } + state.customers.nameEditor.draft = input.value || ''; + state.customers.nameEditor.flash = ''; + if (!state.customers.nameEditor.error) { + return; + } + state.customers.nameEditor.error = ''; + const form = input.closest('[data-customer-name-form]'); + const status = form?.querySelector?.('[data-customer-name-status]'); + if (status) { + status.textContent = 'Имя сохранится в профиле клиента и voice-контуре.'; + status.classList.remove('error'); + } +} + +function handleCustomerProfileNameSubmit(event) { + const form = event.target?.closest?.('[data-customer-name-form]'); + if (!form) { + return; + } + event.preventDefault(); + saveCustomerProfileName().catch((err) => { + state.customers.nameEditor.saving = false; + state.customers.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.'; + renderCustomerSpotlight(); + }); +} + +function wire() { + $('loginBtn').addEventListener('click', login); + $('corporateLoginBtn').addEventListener('click', startCorporateLogin); + $('refreshBtn').addEventListener('click', async () => { + await refreshUnifiedInbox(); + if (operatorHashState().view === 'messages') { + await loadMessengerThreads(false, { preserveSelection: true, preserveOnError: true }); + } + if (featureEnabled('whatsapp')) { + await loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true }); + } + log('Данные на экране обновлены'); + }); + $('refreshUnifiedInboxBtn').addEventListener('click', () => { + refreshUnifiedInbox().catch((err) => { + log('Не удалось обновить единую очередь', { error: err.message }); + }); + }); + $('unifiedInboxBoard').addEventListener('click', handleUnifiedInboxClick); + $('toggleCustomerLeadFormBtn').addEventListener('click', () => toggleCustomerLeadForm()); + $('customerCancelBtn').addEventListener('click', () => toggleCustomerLeadForm(false)); + $('createCustomerBtn').addEventListener('click', createCustomer); + $('exportCustomersBtn').addEventListener('click', exportCustomersCsv); + $('customerPrevPageBtn').addEventListener('click', () => setCustomerPage(state.customers.page - 1)); + $('customerNextPageBtn').addEventListener('click', () => setCustomerPage(state.customers.page + 1)); + $('customerQuery').addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + searchCustomers(); + } + }); + $('customerList').addEventListener('click', (event) => { + const row = event.target.closest('[data-customer-id]'); + if (!row) { + return; + } + openCustomerProfile(row.dataset.customerId || ''); + }); + $('customerList').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' && event.key !== ' ') { + return; + } + const row = event.target.closest('[data-customer-id]'); + if (!row) { + return; + } + event.preventDefault(); + openCustomerProfile(row.dataset.customerId || ''); + }); + $('customerSpotlight')?.addEventListener('click', handleCustomerActionClick); + $('customerSpotlight')?.addEventListener('input', handleCustomerProfileNameInput); + $('customerSpotlight')?.addEventListener('submit', handleCustomerProfileNameSubmit); + $('customerProfileContent').addEventListener('click', handleCustomerActionClick); + $('customerProfileContent').addEventListener('input', handleCustomerProfileNameInput); + $('customerProfileContent').addEventListener('submit', handleCustomerProfileNameSubmit); + $('customerProfileBackBtn').addEventListener('click', () => { + window.location.hash = '#customers-page'; + }); + $('messengerRefreshBtn').addEventListener('click', () => { + loadMessengerThreads(true, { preserveSelection: true, preserveOnError: true }).catch(() => {}); + }); + $('messengerRetryBtn').addEventListener('click', () => { + loadMessengerThreads(true, { preserveSelection: true, preserveOnError: true }).catch(() => {}); + }); + document.querySelectorAll('[data-messenger-filter]').forEach((button) => { + button.addEventListener('click', () => setMessengerFilter(button.dataset.messengerFilter || 'all')); + }); + $('messengerConversationList').addEventListener('click', (event) => { + const button = event.target.closest('[data-messenger-conversation-id]'); + if (!button) { + return; + } + selectMessengerConversation(button.dataset.messengerConversationId || '').catch(() => {}); + }); + $('messengerConversationList').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' && event.key !== ' ') { + return; + } + const button = event.target.closest('[data-messenger-conversation-id]'); + if (!button) { + return; + } + event.preventDefault(); + selectMessengerConversation(button.dataset.messengerConversationId || '').catch(() => {}); + }); + $('messengerClaimBtn').addEventListener('click', () => { + claimMessengerConversation().catch(() => {}); + }); + $('messengerCloseBtn').addEventListener('click', () => { + closeMessengerConversation().catch(() => {}); + }); + $('messengerOpenProfileBtn').addEventListener('click', openMessengerCustomerProfile); + $('messengerContextOpenProfileBtn').addEventListener('click', openMessengerCustomerProfile); + $('messengerSendReplyBtn').addEventListener('click', () => { + sendMessengerReply().catch(() => {}); + }); + $('messengerReplyText').addEventListener('input', (event) => { + state.messenger.composerText = event.target.value || ''; + syncMessengerComposerHeight(); + syncMessengerActionButtons(); + }); + $('messengerReplyText').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' || event.shiftKey) { + return; + } + event.preventDefault(); + sendMessengerReply().catch(() => {}); + }); + $('loadTelegramThreadsBtn').addEventListener('click', () => loadTelegramThreads(true, { preserveSelection: true, preserveOnError: true })); + $('telegramThreadSearch').addEventListener('input', (event) => { + state.telegram.searchQuery = event.target.value || ''; + renderTelegramWorkspace(); + }); + $('telegramHeaderSearchBtn').addEventListener('click', () => { + toggleTelegramMessageSearch(); + }); + $('telegramMessageSearch').addEventListener('input', (event) => { + state.telegram.messageSearchQuery = event.target.value || ''; + renderTelegramWorkspace(); + }); + $('telegramMessageSearch').addEventListener('keydown', (event) => { + if (event.key !== 'Escape') { + return; + } + event.preventDefault(); + if (telegramMessageSearchQuery()) { + state.telegram.messageSearchQuery = ''; + renderTelegramWorkspace(); + return; + } + toggleTelegramMessageSearch(false); + }); + $('telegramMessageSearchClearBtn').addEventListener('click', () => { + if (telegramMessageSearchQuery()) { + state.telegram.messageSearchQuery = ''; + renderTelegramWorkspace(); + $('telegramMessageSearch').focus(); + return; + } + toggleTelegramMessageSearch(false); + }); + $('telegramHeaderCallBtn').addEventListener('click', () => { + log('Voice call action для Telegram пока не назначен'); + }); + $('telegramHeaderMenuBtn').addEventListener('click', () => { + toggleTelegramOperatorTray(); + }); + $('telegramThreadsList').addEventListener('click', (event) => { + const button = event.target.closest('[data-telegram-thread-id]'); + if (!button) { + return; + } + selectTelegramThread(button.dataset.telegramThreadId || ''); + }); + $('telegramThreadsList').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' && event.key !== ' ') { + return; + } + const button = event.target.closest('[data-telegram-thread-id]'); + if (!button) { + return; + } + event.preventDefault(); + selectTelegramThread(button.dataset.telegramThreadId || ''); + }); + $('telegramClaimThreadBtn').addEventListener('click', claimTelegramThread); + $('telegramReturnToAiBtn').addEventListener('click', returnTelegramThreadToAi); + $('telegramCloseThreadBtn').addEventListener('click', closeTelegramThread); + $('telegramEscalateThreadBtn').addEventListener('click', escalateTelegramThread); + $('telegramSendReplyBtn').addEventListener('click', sendTelegramReply); + $('telegramReplyText').addEventListener('input', () => { + syncTelegramComposerHeight(); + syncTelegramActionButtons(); + }); + $('telegramReplyText').addEventListener('keydown', (event) => { + if (event.key !== 'Enter') { + return; + } + if (event.shiftKey) { + return; + } + event.preventDefault(); + sendTelegramReply(); + }); + if (featureEnabled('whatsapp')) { + $('whatsappSearchInput').addEventListener('input', (event) => { + state.whatsapp.searchQuery = event.target.value || ''; + renderWhatsappWorkspace(); + }); + $('whatsappFilterBar').addEventListener('click', (event) => { + const button = event.target.closest('[data-whatsapp-filter]'); + if (!button) { + return; + } + setWhatsappFilter(button.dataset.whatsappFilter || 'all'); + }); + $('whatsappClaimBtn').addEventListener('click', () => { + claimWhatsappThread().catch(() => {}); + }); + $('whatsappReturnToAiBtn').addEventListener('click', returnWhatsappThreadToAi); + $('whatsappChatList').addEventListener('click', (event) => { + const button = event.target.closest('[data-whatsapp-chat-id]'); + if (!button) { + return; + } + selectWhatsappChat(button.dataset.whatsappChatId || '').catch(() => {}); + }); + $('whatsappChatList').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' && event.key !== ' ') { + return; + } + const button = event.target.closest('[data-whatsapp-chat-id]'); + if (!button) { + return; + } + event.preventDefault(); + selectWhatsappChat(button.dataset.whatsappChatId || '').catch(() => {}); + }); + document.querySelectorAll('[data-whatsapp-ui-action]').forEach((button) => { + button.addEventListener('click', handleWhatsappUiAction); + }); + $('whatsappSendBtn').addEventListener('click', sendWhatsappMessage); + $('whatsappComposerInput').addEventListener('input', (event) => { + state.whatsapp.composerText = event.target.value || ''; + syncWhatsappComposerUi(); + }); + $('whatsappComposerInput').addEventListener('keydown', (event) => { + if (event.key !== 'Enter' || event.shiftKey) { + return; + } + event.preventDefault(); + sendWhatsappMessage(); + }); + } + $('createInteractionBtn').addEventListener('click', createInteraction); + $('loadInteractionsBtn').addEventListener('click', loadInteractions); + $('loadLiveCallsBtn').addEventListener('click', () => loadLiveCalls(true)); + $('loadLiveCallActionsBtn').addEventListener('click', loadLiveCallActions); + $('liveCallsTable').addEventListener('click', handleLiveCallTableClick); + $('liveCallNameInput').addEventListener('input', (event) => { + state.liveCalls.nameEditor.draft = event.target.value || ''; + if (state.liveCalls.nameEditor.error) { + state.liveCalls.nameEditor.error = ''; + } + updateLiveCallNameEditorsUi(); + }); + $('liveCallNameSaveBtn').addEventListener('click', () => { + saveLiveCallCustomerName().catch((err) => { + state.liveCalls.nameEditor.saving = false; + state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.'; + updateLiveCallNameEditorsUi(); + }); + }); + $('liveCallNameCancelBtn').addEventListener('click', closeLiveCallNameEditor); + $('browserPhoneStatusBtn').addEventListener('click', toggleBrowserPhoneSettings); + $('browserPhoneCallSettingsBtn').addEventListener('click', toggleBrowserPhoneSettings); + $('browserPhoneConnectBtn').addEventListener('click', connectBrowserSoftphone); + $('browserPhoneDisconnectBtn').addEventListener('click', disconnectBrowserSoftphone); + $('browserPhoneMuteBtn').addEventListener('click', toggleBrowserSoftphoneMute); + $('browserPhoneAnswerBtn').addEventListener('click', answerBrowserSoftphoneCall); + $('browserPhoneRejectBtn').addEventListener('click', rejectBrowserSoftphoneCall); + $('browserPhoneCallClaimBtn').addEventListener('click', claimBrowserPopupCall); + $('browserPhoneEditNameBtn').addEventListener('click', () => { + const popupCall = browserPhoneActivePopupCall(); + if (popupCall?.call_id) { + openLiveCallNameEditor(popupCall.call_id, 'popup'); + } + }); + $('browserPhoneCallTransferBtn').addEventListener('click', transferBrowserPopupCall); + $('browserPhoneCallHangupBtn').addEventListener('click', hangupBrowserPopupCall); + $('browserPhoneNameInput').addEventListener('input', (event) => { + state.liveCalls.nameEditor.draft = event.target.value || ''; + if (state.liveCalls.nameEditor.error) { + state.liveCalls.nameEditor.error = ''; + } + updateLiveCallNameEditorsUi(); + }); + $('browserPhoneNameSaveBtn').addEventListener('click', () => { + saveLiveCallCustomerName().catch((err) => { + state.liveCalls.nameEditor.saving = false; + state.liveCalls.nameEditor.error = err.message || 'Не удалось сохранить имя клиента.'; + updateLiveCallNameEditorsUi(); + }); + }); + $('browserPhoneNameCancelBtn').addEventListener('click', closeLiveCallNameEditor); + $('browserPhoneMicSelect').addEventListener('change', () => { + state.browserPhone.micDeviceId = $('browserPhoneMicSelect').value; + stopBrowserPhoneLocalStream(); + if (state.browserPhone.incoming) { + prepareBrowserPhoneAnswerMedia(); + } + }); + $('browserPhoneSpeakerSelect').addEventListener('change', () => { + state.browserPhone.speakerDeviceId = $('browserPhoneSpeakerSelect').value; + syncBrowserPhoneSpeakerSink(); + }); + $('browserPhoneTransferTargetValue').addEventListener('input', updateBrowserPhoneUi); + $('browserPhoneTransferTargetType').addEventListener('change', updateBrowserPhoneUi); + $('liveCallIdSelect').addEventListener('change', () => { + state.liveCalls.selectedCallId = $('liveCallIdSelect').value; + syncLiveCallActionButtons(); + }); + $('logoutBtn').addEventListener('click', logout); + window.addEventListener('message', handleOidcMessage); + window.addEventListener('hashchange', handleOperatorHashChange); + if (navigator.mediaDevices?.addEventListener) { + navigator.mediaDevices.addEventListener('devicechange', () => { + loadBrowserPhoneDevices().catch(() => {}); + }); + } +} + +async function init() { + if (!restoreStoredSession()) { + window.location.href = '/'; + return; + } + await loadOperatorConfig(); + wire(); + applyOperatorViewFromHash(); + $('defaultAssignee').value = DEMO_ASSIGNEE; + $('defaultEscalationQueue').value = DEMO_QUEUE; + $('telegramEscalationQueue').value = DEMO_QUEUE; + updateSessionInfo(); + syncLiveCallActionButtons(); + renderUnifiedInbox(); + renderMessengerWorkspace(); + if (featureEnabled('whatsapp')) { + renderWhatsappWorkspace(); + } + renderTelegramWorkspace(); + await Promise.all([checkGateway(), loadOidcConfig()]); + await fetchBrowserSoftphoneConfig(); + await loadBrowserPhoneDevices().catch(() => {}); + syncCustomerFormUi(); + await searchCustomers(); + await loadInteractions(); + await loadTelegramThreads(false, { preserveSelection: true, preserveOnError: true }); + if (featureEnabled('whatsapp')) { + await loadWhatsappThreads(false, { preserveSelection: true, preserveOnError: true }); + } + await loadLiveCalls(false); + startLiveCallsPolling(); + startTelegramPolling(); + if (featureEnabled('whatsapp')) { + startWhatsappPolling(); + } + log('Экран готов к показу', { assignee: DEMO_ASSIGNEE, queue: DEMO_QUEUE }); +} + +init(); diff --git a/ui/operator/favicon.svg b/ui/operator/favicon.svg new file mode 100644 index 0000000..927bd5b --- /dev/null +++ b/ui/operator/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/operator/index.html b/ui/operator/index.html new file mode 100644 index 0000000..addfb27 --- /dev/null +++ b/ui/operator/index.html @@ -0,0 +1,762 @@ + + + + + + KonturCC Оператор + + + + + + + +
            + + +
            +
            + +
            +
            Шлюз: проверка...
            +
            + + +
            + +
            +
            OP
            +
            +
            Экран оператора
            +
            Основной сценарий
            +
            +
            +
            +
            + +
            +
            +
            +

            Операторский пульт v2

            +
            + +
            +

            Сессия

            +
            + + +
            +
            + + +
            +
            + + +
            +
            + + + +
            +

            Токен: отсутствует

            +
            + +
            +
            +
            +
            +

            Единая очередь

            +

            Новые задачи, мои диалоги, AI handoff, эскалации и живые звонки в одном месте.

            +
            + +
            +
            +
            +
            + +
            +

            Обращения

            +
            + + + + +
            +
            + + +
            +
            + +
            +
            +
            + +
            +
            + + + + + + + + + + + + +
            +
            +
            + + + + + + + + + + diff --git a/ui/operator/sip-0.21.2.min.js b/ui/operator/sip-0.21.2.min.js new file mode 100644 index 0000000..5b917b4 --- /dev/null +++ b/ui/operator/sip-0.21.2.min.js @@ -0,0 +1,2 @@ +/*! For license information please see sip.min.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SIP=t():e.SIP=t()}(this,(()=>(()=>{"use strict";var e={d:(t,s)=>{for(var i in s)e.o(s,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:s[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Ack:()=>l,Bye:()=>g,Cancel:()=>u,ContentTypeUnsupportedError:()=>o,Core:()=>s,EmitterImpl:()=>p,Grammar:()=>y,Info:()=>f,Invitation:()=>Q,Inviter:()=>ee,Message:()=>W,Messager:()=>te,NameAddrHeader:()=>v,Notification:()=>K,Parameters:()=>m,Publisher:()=>le,PublisherState:()=>se,Referral:()=>Y,Registerer:()=>ge,RegistererState:()=>ie,RequestPendingError:()=>a,SIPExtension:()=>J,Session:()=>z,SessionDescriptionHandlerError:()=>c,SessionState:()=>Z,SessionTerminatedError:()=>h,StateTransitionError:()=>d,Subscriber:()=>pe,Subscription:()=>ue,SubscriptionState:()=>ne,TransportState:()=>oe,URI:()=>w,UserAgent:()=>dt,UserAgentRegisteredOptionTags:()=>X,UserAgentState:()=>ae,Web:()=>i,equivalentURI:()=>b,name:()=>At,version:()=>Ct});var s={};e.r(s),e.d(s,{ByeUserAgentClient:()=>Pe,ByeUserAgentServer:()=>_e,C:()=>G,CancelUserAgentClient:()=>lt,ClientTransaction:()=>Ie,Dialog:()=>Ae,DigestAuthentication:()=>we,Exception:()=>n,Grammar:()=>y,IncomingMessage:()=>H,IncomingRequestMessage:()=>P,IncomingResponseMessage:()=>x,InfoUserAgentClient:()=>qe,InfoUserAgentServer:()=>Me,InviteClientTransaction:()=>De,InviteServerTransaction:()=>$e,InviteUserAgentClient:()=>Ye,InviteUserAgentServer:()=>Ze,Levels:()=>ce,Logger:()=>be,LoggerFactory:()=>Te,MessageUserAgentClient:()=>Ne,MessageUserAgentServer:()=>Oe,NameAddrHeader:()=>v,NonInviteClientTransaction:()=>Ce,NonInviteServerTransaction:()=>xe,NotifyUserAgentClient:()=>Ue,NotifyUserAgentServer:()=>je,OutgoingRequestMessage:()=>k,Parameters:()=>m,Parser:()=>he,PrackUserAgentClient:()=>Fe,PrackUserAgentServer:()=>Le,PublishUserAgentClient:()=>Je,ReInviteUserAgentClient:()=>Be,ReInviteUserAgentServer:()=>Ge,ReSubscribeUserAgentClient:()=>Qe,ReSubscribeUserAgentServer:()=>gt,ReferUserAgentClient:()=>Ve,ReferUserAgentServer:()=>We,RegisterUserAgentClient:()=>ze,RegisterUserAgentServer:()=>Xe,ServerTransaction:()=>Ee,SessionDialog:()=>Ke,SessionState:()=>O,SignalingState:()=>U,SubscribeUserAgentClient:()=>tt,SubscribeUserAgentServer:()=>st,SubscriptionDialog:()=>et,SubscriptionState:()=>re,Timers:()=>L,Transaction:()=>ye,TransactionState:()=>de,TransactionStateError:()=>B,TransportError:()=>Re,URI:()=>w,UserAgentClient:()=>He,UserAgentCore:()=>rt,UserAgentServer:()=>ke,constructOutgoingResponse:()=>Se,equivalentURI:()=>b,fromBodyLegacy:()=>q,getBody:()=>N,isBody:()=>M});var i={};e.r(i),e.d(i,{SessionDescriptionHandler:()=>at,SessionManager:()=>$t,SimpleUser:()=>It,Transport:()=>ht,WebAudioSessionDescriptionHandler:()=>yt,addMidLines:()=>Tt,cleanJitsiSdpImageattr:()=>mt,defaultManagedSessionFactory:()=>Et,defaultMediaStreamFactory:()=>nt,defaultPeerConnectionConfiguration:()=>ot,defaultSessionDescriptionHandlerFactory:()=>ct,holdModifier:()=>St,startLocalConference:()=>Rt,stripG722:()=>vt,stripRtpPayload:()=>wt,stripTcpCandidates:()=>pt,stripTelephoneEvent:()=>ft,stripVideo:()=>bt});const r="0.21.2";class n extends Error{constructor(e){super(e),Object.setPrototypeOf(this,new.target.prototype)}}class o extends n{constructor(e){super(e||"Unsupported content type.")}}class a extends n{constructor(e){super(e||"Request pending.")}}class c extends n{constructor(e){super(e||"Unspecified session description handler error.")}}class h extends n{constructor(){super("The session has terminated.")}}class d extends n{constructor(e){super(e||"An error occurred during state transition.")}}class l{constructor(e){this.incomingAckRequest=e}get request(){return this.incomingAckRequest.message}}class g{constructor(e){this.incomingByeRequest=e}get request(){return this.incomingByeRequest.message}accept(e){return this.incomingByeRequest.accept(e),Promise.resolve()}reject(e){return this.incomingByeRequest.reject(e),Promise.resolve()}}class u{constructor(e){this.incomingCancelRequest=e}get request(){return this.incomingCancelRequest}}class p{constructor(){this.listeners=new Array}addListener(e,t){const s=t=>{this.removeListener(s),e(t)};!0===(null==t?void 0:t.once)?this.listeners.push(s):this.listeners.push(e)}emit(e){this.listeners.slice().forEach((t=>t(e)))}removeAllListeners(){this.listeners=[]}removeListener(e){this.listeners=this.listeners.filter((t=>t!==e))}on(e){return this.addListener(e)}off(e){return this.removeListener(e)}once(e){return this.addListener(e,{once:!0})}}class f{constructor(e){this.incomingInfoRequest=e}get request(){return this.incomingInfoRequest.message}accept(e){return this.incomingInfoRequest.accept(e),Promise.resolve()}reject(e){return this.incomingInfoRequest.reject(e),Promise.resolve()}}class m{constructor(e){this.parameters={};for(const t in e)e.hasOwnProperty(t)&&this.setParam(t,e[t])}setParam(e,t){e&&(this.parameters[e.toLowerCase()]=null==t?null:t.toString())}getParam(e){if(e)return this.parameters[e.toLowerCase()]}hasParam(e){return!(!e||void 0===this.parameters[e.toLowerCase()])}deleteParam(e){if(e=e.toLowerCase(),this.hasParam(e)){const t=this.parameters[e];return delete this.parameters[e],t}}clearParams(){this.parameters={}}}class v extends m{constructor(e,t,s){super(s),this.uri=e,this._displayName=t}get friendlyName(){return this.displayName||this.uri.aor}get displayName(){return this._displayName}set displayName(e){this._displayName=e}clone(){return new v(this.uri.clone(),this._displayName,JSON.parse(JSON.stringify(this.parameters)))}toString(){let e=this.displayName||"0"===this.displayName?'"'+this.displayName+'" ':"";e+="<"+this.uri.toString()+">";for(const t in this.parameters)this.parameters.hasOwnProperty(t)&&(e+=";"+t,null!==this.parameters[t]&&(e+="="+this.parameters[t]));return e}}class w extends m{constructor(e="sip",t,s,i,r,n){if(super(r||{}),this.headers={},!s)throw new TypeError('missing or invalid "host" parameter');for(const e in n)n.hasOwnProperty(e)&&this.setHeader(e,n[e]);this.raw={scheme:e,user:t,host:s,port:i},this.normal={scheme:e.toLowerCase(),user:t,host:s.toLowerCase(),port:i}}get scheme(){return this.normal.scheme}set scheme(e){this.raw.scheme=e,this.normal.scheme=e.toLowerCase()}get user(){return this.normal.user}set user(e){this.normal.user=this.raw.user=e}get host(){return this.normal.host}set host(e){this.raw.host=e,this.normal.host=e.toLowerCase()}get aor(){return this.normal.user+"@"+this.normal.host}get port(){return this.normal.port}set port(e){this.normal.port=this.raw.port=e}setHeader(e,t){this.headers[this.headerize(e)]=t instanceof Array?t:[t]}getHeader(e){if(e)return this.headers[this.headerize(e)]}hasHeader(e){return!!e&&!!this.headers.hasOwnProperty(this.headerize(e))}deleteHeader(e){if(e=this.headerize(e),this.headers.hasOwnProperty(e)){const t=this.headers[e];return delete this.headers[e],t}}clearHeaders(){this.headers={}}clone(){return new w(this._raw.scheme,this._raw.user||"",this._raw.host,this._raw.port,JSON.parse(JSON.stringify(this.parameters)),JSON.parse(JSON.stringify(this.headers)))}toRaw(){return this._toString(this._raw)}toString(){return this._toString(this._normal)}get _normal(){return this.normal}get _raw(){return this.raw}_toString(e){let t=e.scheme+":";e.scheme.toLowerCase().match("^sips?$")||(t+="//"),e.user&&(t+=this.escapeUser(e.user)+"@"),t+=e.host,(e.port||0===e.port)&&(t+=":"+e.port);for(const e in this.parameters)this.parameters.hasOwnProperty(e)&&(t+=";"+e,null!==this.parameters[e]&&(t+="="+this.parameters[e]));const s=[];for(const e in this.headers)if(this.headers.hasOwnProperty(e))for(const t in this.headers[e])this.headers[e].hasOwnProperty(t)&&s.push(e+"="+this.headers[e][t]);return s.length>0&&(t+="?"+s.join("&")),t}escapeUser(e){let t;try{t=decodeURIComponent(e)}catch(e){throw e}return encodeURIComponent(t).replace(/%3A/gi,":").replace(/%2B/gi,"+").replace(/%3F/gi,"?").replace(/%2F/gi,"/")}headerize(e){const t={"Call-Id":"Call-ID",Cseq:"CSeq","Min-Se":"Min-SE",Rack:"RAck",Rseq:"RSeq","Www-Authenticate":"WWW-Authenticate"},s=e.toLowerCase().replace(/_/g,"-").split("-"),i=s.length;let r="";for(let e=0;ei.includes(e))).every((s=>e.parameters[s]===t.parameters[s]))&&(!!["user","ttl","method","transport"].every((s=>e.hasParam(s)&&t.hasParam(s)||!e.hasParam(s)&&!t.hasParam(s)))&&!!["maddr"].every((s=>e.hasParam(s)&&t.hasParam(s)||!e.hasParam(s)&&!t.hasParam(s))))}(e,t))return!1;const s=Object.keys(e.headers),i=Object.keys(t.headers);if(0!==s.length||0!==i.length){if(s.length!==i.length)return!1;const r=s.filter((e=>i.includes(e)));if(r.length!==i.length)return!1;if(!r.every((s=>e.headers[s].length&&t.headers[s].length&&e.headers[s][0]===t.headers[s][0])))return!1}return!0}function T(e,t,s){return s=s||" ",e.length>t?e:(t-=e.length,e+(s+=s.repeat(t)).slice(0,t))}class S extends Error{constructor(e,t,s,i){super(),this.message=e,this.expected=t,this.found=s,this.location=i,this.name="SyntaxError","function"==typeof Object.setPrototypeOf?Object.setPrototypeOf(this,S.prototype):this.__proto__=S.prototype,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,S)}static buildMessage(e,t){function s(e){return e.charCodeAt(0).toString(16).toUpperCase()}function i(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\0/g,"\\0").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/[\x00-\x0F]/g,(e=>"\\x0"+s(e))).replace(/[\x10-\x1F\x7F-\x9F]/g,(e=>"\\x"+s(e)))}function r(e){return e.replace(/\\/g,"\\\\").replace(/\]/g,"\\]").replace(/\^/g,"\\^").replace(/-/g,"\\-").replace(/\0/g,"\\0").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/[\x00-\x0F]/g,(e=>"\\x0"+s(e))).replace(/[\x10-\x1F\x7F-\x9F]/g,(e=>"\\x"+s(e)))}function n(e){switch(e.type){case"literal":return'"'+i(e.text)+'"';case"class":const t=e.parts.map((e=>Array.isArray(e)?r(e[0])+"-"+r(e[1]):r(e)));return"["+(e.inverted?"^":"")+t+"]";case"any":return"any character";case"end":return"end of input";case"other":return e.description}}return"Expected "+function(e){const t=e.map(n);let s,i;if(t.sort(),t.length>0){for(s=1,i=1;s",b(">",!1),"\\",b("\\",!1),"[",b("[",!1),"]",b("]",!1),"{",b("{",!1),"}",b("}",!1),function(){return"*"},function(){return"/"},function(){return"="},function(){return"("},function(){return")"},function(){return">"},function(){return"<"},function(){return","},function(){return";"},function(){return":"},function(){return'"'},/^[!-']/,T([["!","'"]],!1,!1),/^[*-[]/,T([["*","["]],!1,!1),/^[\]-~]/,T([["]","~"]],!1,!1),function(e){return e},/^[#-[]/,T([["#","["]],!1,!1),/^[\0-\t]/,T([["\0","\t"]],!1,!1),/^[\v-\f]/,T([["\v","\f"]],!1,!1),/^[\x0E-\x7F]/,T([["\x0e","\x7f"]],!1,!1),function(){(t=t||{data:{}}).data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port},function(){(t=t||{data:{}}).data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port,t.data.uri_params,t.data.uri_headers),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port,delete t.data.uri_params,"SIP_URI"===t.startRule&&(t.data=t.data.uri)},"sips",b("sips",!0),"sip",b("sip",!0),function(e){(t=t||{data:{}}).data.scheme=e},function(){(t=t||{data:{}}).data.user=decodeURIComponent(f().slice(0,-1))},function(){(t=t||{data:{}}).data.password=f()},function(){return(t=t||{data:{}}).data.host=f(),t.data.host},function(){return(t=t||{data:{}}).data.host_type="domain",f()},/^[a-zA-Z0-9_\-]/,T([["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),/^[a-zA-Z0-9\-]/,T([["a","z"],["A","Z"],["0","9"],"-"],!1,!1),function(){return(t=t||{data:{}}).data.host_type="IPv6",f()},"::",b("::",!1),function(){return(t=t||{data:{}}).data.host_type="IPv6",f()},function(){return(t=t||{data:{}}).data.host_type="IPv4",f()},"25",b("25",!1),/^[0-5]/,T([["0","5"]],!1,!1),"2",b("2",!1),/^[0-4]/,T([["0","4"]],!1,!1),"1",b("1",!1),/^[1-9]/,T([["1","9"]],!1,!1),function(e){return t=t||{data:{}},e=parseInt(e.join("")),t.data.port=e,e},"transport=",b("transport=",!0),"udp",b("udp",!0),"tcp",b("tcp",!0),"sctp",b("sctp",!0),"tls",b("tls",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.transport=e.toLowerCase()},"user=",b("user=",!0),"phone",b("phone",!0),"ip",b("ip",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.user=e.toLowerCase()},"method=",b("method=",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.method=e},"ttl=",b("ttl=",!0),function(e){(t=t||{data:{}}).data.params||(t.data.params={}),t.data.params.ttl=e},"maddr=",b("maddr=",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.maddr=e},"lr",b("lr",!0),function(){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.lr=void 0},function(e,s){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),s=null===s?void 0:s[1],t.data.uri_params[e.toLowerCase()]=s},function(e,s){e=e.join("").toLowerCase(),s=s.join(""),(t=t||{data:{}}).data.uri_headers||(t.data.uri_headers={}),t.data.uri_headers[e]?t.data.uri_headers[e].push(s):t.data.uri_headers[e]=[s]},function(){"Refer_To"===(t=t||{data:{}}).startRule&&(t.data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port,t.data.uri_params,t.data.uri_headers),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port,delete t.data.uri_params)},"//",b("//",!1),function(){(t=t||{data:{}}).data.scheme=f()},b("SIP",!0),function(){(t=t||{data:{}}).data.sip_version=f()},"INVITE",b("INVITE",!1),"ACK",b("ACK",!1),"VXACH",b("VXACH",!1),"OPTIONS",b("OPTIONS",!1),"BYE",b("BYE",!1),"CANCEL",b("CANCEL",!1),"REGISTER",b("REGISTER",!1),"SUBSCRIBE",b("SUBSCRIBE",!1),"NOTIFY",b("NOTIFY",!1),"REFER",b("REFER",!1),"PUBLISH",b("PUBLISH",!1),function(){return(t=t||{data:{}}).data.method=f(),t.data.method},function(e){(t=t||{data:{}}).data.status_code=parseInt(e.join(""))},function(){(t=t||{data:{}}).data.reason_phrase=f()},function(){(t=t||{data:{}}).data=f()},function(){var e,s;for(s=(t=t||{data:{}}).data.multi_header.length,e=0;e""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I(";).# &;,"),I('2F""6F7G.} &2H""6H7I.q &2J""6J7K.e &2L""6L7M.Y &2N""6N7O.M &2P""6P7Q.A &2R""6R7S.5 &2T""6T7U.) &2V""6V7W'),I('%%2X""6X7Y/5#;#/,$;#/#$+#)(#\'#("\'#&\'#/"!&,)'),I('%%$;$0#*;$&/,#; /#$+")("\'#&\'#." &"/=#$;$/�#*;$&&&#/\'$8":Z" )("\'#&\'#'),I(';.." &"'),I("%$;'.# &;(0)*;'.# &;(&/?#28\"\"6879/0$;//'$8#:[# )(#'#(\"'#&'#"),I('%%$;2/�#*;2&&&#/g#$%$;.0#*;.&/,#;2/#$+")("\'#&\'#0=*%$;.0#*;.&/,#;2/#$+")("\'#&\'#&/#$+")("\'#&\'#/"!&,)'),I('4\\""5!7].# &;3'),I('4^""5!7_'),I('4`""5!7a'),I(';!.) &4b""5!7c'),I('%$;).\x95 &2F""6F7G.\x89 &2J""6J7K.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O/\x9e#0\x9b*;).\x95 &2F""6F7G.\x89 &2J""6J7K.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O&&&#/"!&,)'),I('%$;).\x89 &2F""6F7G.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O/\x92#0\x8f*;).\x89 &2F""6F7G.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O&&&#/"!&,)'),I('2T""6T7U.\xe3 &2V""6V7W.\xd7 &2f""6f7g.\xcb &2h""6h7i.\xbf &2:""6:7;.\xb3 &2D""6D7E.\xa7 &22""6273.\x9b &28""6879.\x8f &2j""6j7k.\x83 &;&.} &24""6475.q &2l""6l7m.e &2n""6n7o.Y &26""6677.M &2>""6>7?.A &2p""6p7q.5 &2r""6r7s.) &;\'.# &;('),I('%$;).\u012b &2F""6F7G.\u011f &2J""6J7K.\u0113 &2L""6L7M.\u0107 &2X""6X7Y.\xfb &2P""6P7Q.\xef &2H""6H7I.\xe3 &2@""6@7A.\xd7 &2d""6d7e.\xcb &2R""6R7S.\xbf &2N""6N7O.\xb3 &2T""6T7U.\xa7 &2V""6V7W.\x9b &2f""6f7g.\x8f &2h""6h7i.\x83 &28""6879.w &2j""6j7k.k &;&.e &24""6475.Y &2l""6l7m.M &2n""6n7o.A &26""6677.5 &2p""6p7q.) &2r""6r7s/\u0134#0\u0131*;).\u012b &2F""6F7G.\u011f &2J""6J7K.\u0113 &2L""6L7M.\u0107 &2X""6X7Y.\xfb &2P""6P7Q.\xef &2H""6H7I.\xe3 &2@""6@7A.\xd7 &2d""6d7e.\xcb &2R""6R7S.\xbf &2N""6N7O.\xb3 &2T""6T7U.\xa7 &2V""6V7W.\x9b &2f""6f7g.\x8f &2h""6h7i.\x83 &28""6879.w &2j""6j7k.k &;&.e &24""6475.Y &2l""6l7m.M &2n""6n7o.A &26""6677.5 &2p""6p7q.) &2r""6r7s&&&#/"!&,)'),I("%;//?#2P\"\"6P7Q/0$;//'$8#:t# )(#'#(\"'#&'#"),I("%;//?#24\"\"6475/0$;//'$8#:u# )(#'#(\"'#&'#"),I("%;//?#2>\"\"6>7?/0$;//'$8#:v# )(#'#(\"'#&'#"),I("%;//?#2T\"\"6T7U/0$;//'$8#:w# )(#'#(\"'#&'#"),I("%;//?#2V\"\"6V7W/0$;//'$8#:x# )(#'#(\"'#&'#"),I('%2h""6h7i/0#;//\'$8":y" )("\'#&\'#'),I('%;//6#2f""6f7g/\'$8":z" )("\'#&\'#'),I("%;//?#2D\"\"6D7E/0$;//'$8#:{# )(#'#(\"'#&'#"),I("%;//?#22\"\"6273/0$;//'$8#:|# )(#'#(\"'#&'#"),I("%;//?#28\"\"6879/0$;//'$8#:}# )(#'#(\"'#&'#"),I("%;//0#;&/'$8\":~\" )(\"'#&'#"),I("%;&/0#;//'$8\":~\" )(\"'#&'#"),I("%;=/T#$;G.) &;K.# &;F0/*;G.) &;K.# &;F&/,$;>/#$+#)(#'#(\"'#&'#"),I('4\x7f""5!7\x80.A &4\x81""5!7\x82.5 &4\x83""5!7\x84.) &;3.# &;.'),I("%%;//Q#;&/H$$;J.# &;K0)*;J.# &;K&/,$;&/#$+$)($'#(#'#(\"'#&'#/\"!&,)"),I("%;//]#;&/T$%$;J.# &;K0)*;J.# &;K&/\"!&,)/1$;&/($8$:\x85$!!)($'#(#'#(\"'#&'#"),I(';..G &2L""6L7M.; &4\x86""5!7\x87./ &4\x83""5!7\x84.# &;3'),I('%2j""6j7k/J#4\x88""5!7\x89.5 &4\x8a""5!7\x8b.) &4\x8c""5!7\x8d/#$+")("\'#&\'#'),I("%;N/M#28\"\"6879/>$;O.\" &\"/0$;S/'$8$:\x8e$ )($'#(#'#(\"'#&'#"),I("%;N/d#28\"\"6879/U$;O.\" &\"/G$;S/>$;_/5$;l.\" &\"/'$8&:\x8f& )(&'#(%'#($'#(#'#(\"'#&'#"),I('%3\x90""5$7\x91.) &3\x92""5#7\x93/\' 8!:\x94!! )'),I('%;P/]#%28""6879/,#;R/#$+")("\'#&\'#." &"/6$2:""6:7;/\'$8#:\x95# )(#\'#("\'#&\'#'),I("$;+.) &;-.# &;Q/2#0/*;+.) &;-.# &;Q&&&#"),I('2<""6<7=.q &2>""6>7?.e &2@""6@7A.Y &2B""6B7C.M &2D""6D7E.A &22""6273.5 &26""6677.) &24""6475'),I('%$;+._ &;-.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E0e*;+._ &;-.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E&/& 8!:\x96! )'),I('%;T/J#%28""6879/,#;^/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I("%;U.) &;\\.# &;X/& 8!:\x97! )"),I('%$%;V/2#2J""6J7K/#$+")("\'#&\'#0<*%;V/2#2J""6J7K/#$+")("\'#&\'#&/D#;W/;$2J""6J7K." &"/\'$8#:\x98# )(#\'#("\'#&\'#'),I('$4\x99""5!7\x9a/,#0)*4\x99""5!7\x9a&&&#'),I('%4$""5!7%/?#$4\x9b""5!7\x9c0)*4\x9b""5!7\x9c&/#$+")("\'#&\'#'),I('%2l""6l7m/?#;Y/6$2n""6n7o/\'$8#:\x9d# )(#\'#("\'#&\'#'),I('%%;Z/\xb3#28""6879/\xa4$;Z/\x9b$28""6879/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+-)(-\'#(,\'#(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0790 &%2\x9e""6\x9e7\x9f/\xa4#;Z/\x9b$28""6879/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+,)(,\'#(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u06f9 &%2\x9e""6\x9e7\x9f/\x8c#;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+*)(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u067a &%2\x9e""6\x9e7\x9f/t#;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0613 &%2\x9e""6\x9e7\x9f/\\#;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+&)(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u05c4 &%2\x9e""6\x9e7\x9f/D#;Z/;$28""6879/,$;[/#$+$)($\'#(#\'#("\'#&\'#.\u058d &%2\x9e""6\x9e7\x9f/,#;[/#$+")("\'#&\'#.\u056e &%2\x9e""6\x9e7\x9f/,#;Z/#$+")("\'#&\'#.\u054f &%;Z/\x9b#2\x9e""6\x9e7\x9f/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$++)(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u04c7 &%;Z/\xaa#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x83$2\x9e""6\x9e7\x9f/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+*)(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0430 &%;Z/\xb9#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x92$%28""6879/,#;Z/#$+")("\'#&\'#." &"/k$2\x9e""6\x9e7\x9f/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+))()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u038a &%;Z/\xc8#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xa1$%28""6879/,#;Z/#$+")("\'#&\'#." &"/z$%28""6879/,#;Z/#$+")("\'#&\'#." &"/S$2\x9e""6\x9e7\x9f/D$;Z/;$28""6879/,$;[/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u02d5 &%;Z/\xd7#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xb0$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x89$%28""6879/,#;Z/#$+")("\'#&\'#." &"/b$%28""6879/,#;Z/#$+")("\'#&\'#." &"/;$2\x9e""6\x9e7\x9f/,$;[/#$+\')(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0211 &%;Z/\xfe#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xd7$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xb0$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x89$%28""6879/,#;Z/#$+")("\'#&\'#." &"/b$%28""6879/,#;Z/#$+")("\'#&\'#." &"/;$2\x9e""6\x9e7\x9f/,$;Z/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0126 &%;Z/\u011c#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xf5$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xce$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xa7$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x80$%28""6879/,#;Z/#$+")("\'#&\'#." &"/Y$%28""6879/,#;Z/#$+")("\'#&\'#." &"/2$2\x9e""6\x9e7\x9f/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#/& 8!:\xa0! )'),I('%;#/M#;#." &"/?$;#." &"/1$;#." &"/#$+$)($\'#(#\'#("\'#&\'#'),I("%;Z/;#28\"\"6879/,$;Z/#$+#)(#'#(\"'#&'#.# &;\\"),I("%;]/o#2J\"\"6J7K/`$;]/W$2J\"\"6J7K/H$;]/?$2J\"\"6J7K/0$;]/'$8':\xa1' )(''#(&'#(%'#($'#(#'#(\"'#&'#"),I('%2\xa2""6\xa27\xa3/2#4\xa4""5!7\xa5/#$+")("\'#&\'#.\x98 &%2\xa6""6\xa67\xa7/;#4\xa8""5!7\xa9/,$;!/#$+#)(#\'#("\'#&\'#.j &%2\xaa""6\xaa7\xab/5#;!/,$;!/#$+#)(#\'#("\'#&\'#.B &%4\xac""5!7\xad/,#;!/#$+")("\'#&\'#.# &;!'),I('%%;!." &"/[#;!." &"/M$;!." &"/?$;!." &"/1$;!." &"/#$+%)(%\'#($\'#(#\'#("\'#&\'#/\' 8!:\xae!! )'),I('$%22""6273/,#;`/#$+")("\'#&\'#0<*%22""6273/,#;`/#$+")("\'#&\'#&'),I(";a.A &;b.; &;c.5 &;d./ &;e.) &;f.# &;g"),I('%3\xaf""5*7\xb0/a#3\xb1""5#7\xb2.G &3\xb3""5#7\xb4.; &3\xb5""5$7\xb6./ &3\xb7""5#7\xb8.# &;6/($8":\xb9"! )("\'#&\'#'),I('%3\xba""5%7\xbb/I#3\xbc""5%7\xbd./ &3\xbe""5"7\xbf.# &;6/($8":\xc0"! )("\'#&\'#'),I('%3\xc1""5\'7\xc2/1#;\x90/($8":\xc3"! )("\'#&\'#'),I('%3\xc4""5$7\xc5/1#;\xf0/($8":\xc6"! )("\'#&\'#'),I('%3\xc7""5&7\xc8/1#;T/($8":\xc9"! )("\'#&\'#'),I('%3\xca""5"7\xcb/N#%2>""6>7?/,#;6/#$+")("\'#&\'#." &"/\'$8":\xcc" )("\'#&\'#'),I('%;h/P#%2>""6>7?/,#;i/#$+")("\'#&\'#." &"/)$8":\xcd""! )("\'#&\'#'),I('%$;j/�#*;j&&&#/"!&,)'),I('%$;j/�#*;j&&&#/"!&,)'),I(";k.) &;+.# &;-"),I('2l""6l7m.e &2n""6n7o.Y &24""6475.M &28""6879.A &2<""6<7=.5 &2@""6@7A.) &2B""6B7C'),I('%26""6677/n#;m/e$$%2<""6<7=/,#;m/#$+")("\'#&\'#0<*%2<""6<7=/,#;m/#$+")("\'#&\'#&/#$+#)(#\'#("\'#&\'#'),I('%;n/A#2>""6>7?/2$;o/)$8#:\xce#"" )(#\'#("\'#&\'#'),I("$;p.) &;+.# &;-/2#0/*;p.) &;+.# &;-&&&#"),I("$;p.) &;+.# &;-0/*;p.) &;+.# &;-&"),I('2l""6l7m.e &2n""6n7o.Y &24""6475.M &26""6677.A &28""6879.5 &2@""6@7A.) &2B""6B7C'),I(";\x91.# &;r"),I("%;\x90/G#;'/>$;s/5$;'/,$;\x84/#$+%)(%'#($'#(#'#(\"'#&'#"),I(";M.# &;t"),I("%;\x7f/E#28\"\"6879/6$;u.# &;x/'$8#:\xcf# )(#'#(\"'#&'#"),I('%;v.# &;w/J#%26""6677/,#;\x83/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I('%2\xd0""6\xd07\xd1/:#;\x80/1$;w." &"/#$+#)(#\'#("\'#&\'#'),I('%24""6475/,#;{/#$+")("\'#&\'#'),I("%;z/3#$;y0#*;y&/#$+\")(\"'#&'#"),I(";*.) &;+.# &;-"),I(';+.\x8f &;-.\x89 &22""6273.} &26""6677.q &28""6879.e &2:""6:7;.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I('%;|/e#$%24""6475/,#;|/#$+")("\'#&\'#0<*%24""6475/,#;|/#$+")("\'#&\'#&/#$+")("\'#&\'#'),I('%$;~0#*;~&/e#$%22""6273/,#;}/#$+")("\'#&\'#0<*%22""6273/,#;}/#$+")("\'#&\'#&/#$+")("\'#&\'#'),I("$;~0#*;~&"),I(';+.w &;-.q &28""6879.e &2:""6:7;.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I('%%;"/\x87#$;".G &;!.A &2@""6@7A.5 &2F""6F7G.) &2J""6J7K0M*;".G &;!.A &2@""6@7A.5 &2F""6F7G.) &2J""6J7K&/#$+")("\'#&\'#/& 8!:\xd2! )'),I(";\x81.# &;\x82"),I('%%;O/2#2:""6:7;/#$+")("\'#&\'#." &"/,#;S/#$+")("\'#&\'#." &"'),I('$;+.\x83 &;-.} &2B""6B7C.q &2D""6D7E.e &22""6273.Y &28""6879.M &2:""6:7;.A &2<""6<7=.5 &2>""6>7?.) &2@""6@7A/\x8c#0\x89*;+.\x83 &;-.} &2B""6B7C.q &2D""6D7E.e &22""6273.Y &28""6879.M &2:""6:7;.A &2<""6<7=.5 &2>""6>7?.) &2@""6@7A&&&#'),I("$;y0#*;y&"),I('%3\x92""5#7\xd3/q#24""6475/b$$;!/�#*;!&&&#/L$2J""6J7K/=$$;!/�#*;!&&&#/\'$8%:\xd4% )(%\'#($\'#(#\'#("\'#&\'#'),I('2\xd5""6\xd57\xd6'),I('2\xd7""6\xd77\xd8'),I('2\xd9""6\xd97\xda'),I('2\xdb""6\xdb7\xdc'),I('2\xdd""6\xdd7\xde'),I('2\xdf""6\xdf7\xe0'),I('2\xe1""6\xe17\xe2'),I('2\xe3""6\xe37\xe4'),I('2\xe5""6\xe57\xe6'),I('2\xe7""6\xe77\xe8'),I('2\xe9""6\xe97\xea'),I("%;\x85.Y &;\x86.S &;\x88.M &;\x89.G &;\x8a.A &;\x8b.; &;\x8c.5 &;\x8f./ &;\x8d.) &;\x8e.# &;6/& 8!:\xeb! )"),I("%;\x84/G#;'/>$;\x92/5$;'/,$;\x94/#$+%)(%'#($'#(#'#(\"'#&'#"),I("%;\x93/' 8!:\xec!! )"),I("%;!/5#;!/,$;!/#$+#)(#'#(\"'#&'#"),I("%$;*.A &;+.; &;-.5 &;3./ &;4.) &;'.# &;(0G*;*.A &;+.; &;-.5 &;3./ &;4.) &;'.# &;(&/& 8!:\xed! )"),I("%;\xb6/Y#$%;A/,#;\xb6/#$+\")(\"'#&'#06*%;A/,#;\xb6/#$+\")(\"'#&'#&/#$+\")(\"'#&'#"),I('%;9/N#%2:""6:7;/,#;9/#$+")("\'#&\'#." &"/\'$8":\xee" )("\'#&\'#'),I("%;:.c &%;\x98/Y#$%;A/,#;\x98/#$+\")(\"'#&'#06*%;A/,#;\x98/#$+\")(\"'#&'#&/#$+\")(\"'#&'#/& 8!:\xef! )"),I("%;L.# &;\x99/]#$%;B/,#;\x9b/#$+\")(\"'#&'#06*%;B/,#;\x9b/#$+\")(\"'#&'#&/'$8\":\xf0\" )(\"'#&'#"),I("%;\x9a.\" &\"/>#;@/5$;M/,$;?/#$+$)($'#(#'#(\"'#&'#"),I("%%;6/Y#$%;./,#;6/#$+\")(\"'#&'#06*%;./,#;6/#$+\")(\"'#&'#&/#$+\")(\"'#&'#.# &;H/' 8!:\xf1!! )"),I(";\x9c.) &;\x9d.# &;\xa0"),I("%3\xf2\"\"5!7\xf3/:#;$;\xcf/5$;./,$;\x90/#$+%)(%'#($'#(#'#(\"'#&'#"),I("%$;!/�#*;!&&&#/' 8!:\u014b!! )"),I("%;\xd1/]#$%;A/,#;\xd1/#$+\")(\"'#&'#06*%;A/,#;\xd1/#$+\")(\"'#&'#&/'$8\":\u014c\" )(\"'#&'#"),I("%;\x99/]#$%;B/,#;\xa0/#$+\")(\"'#&'#06*%;B/,#;\xa0/#$+\")(\"'#&'#&/'$8\":\u014d\" )(\"'#&'#"),I('%;L.O &;\x99.I &%;@." &"/:#;t/1$;?." &"/#$+#)(#\'#("\'#&\'#/]#$%;B/,#;\xa0/#$+")("\'#&\'#06*%;B/,#;\xa0/#$+")("\'#&\'#&/\'$8":\u014e" )("\'#&\'#'),I("%;\xd4/]#$%;B/,#;\xd5/#$+\")(\"'#&'#06*%;B/,#;\xd5/#$+\")(\"'#&'#&/'$8\":\u014f\" )(\"'#&'#"),I("%;\x96/& 8!:\u0150! )"),I('%3\u0151""5(7\u0152/:#;$;6/5$;;/,$;\xec/#$+%)(%'#($'#(#'#(\"'#&'#"),I('%3\x92""5#7\xd3.# &;6/\' 8!:\u018b!! )'),I('%3\xb1""5#7\u018c.G &3\xb3""5#7\u018d.; &3\xb7""5#7\u018e./ &3\xb5""5$7\u018f.# &;6/\' 8!:\u0190!! )'),I('%;\xee/D#%;C/,#;\xef/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I("%;U.) &;\\.# &;X/& 8!:\u0191! )"),I('%%;!." &"/[#;!." &"/M$;!." &"/?$;!." &"/1$;!." &"/#$+%)(%\'#($\'#(#\'#("\'#&\'#/\' 8!:\u0192!! )'),I('%%;!/?#;!." &"/1$;!." &"/#$+#)(#\'#("\'#&\'#/\' 8!:\u0193!! )'),I(";\xbe"),I('%;\x9e/^#$%;B/,#;\xf3/#$+")("\'#&\'#06*%;B/,#;\xf3/#$+")("\'#&\'#&/($8":\u0194"!!)("\'#&\'#'),I(";\xf4.# &;\xa0"),I('%2\u0195""6\u01957\u0196/L#;""6>7?'),I('%;\u0100/b#28""6879/S$;\xfb/J$%2\u01a3""6\u01a37\u01a4/,#;\xec/#$+")("\'#&\'#." &"/#$+$)($\'#(#\'#("\'#&\'#'),I('%3\u01a5""5%7\u01a6.) &3\u01a7""5$7\u01a8/\' 8!:\u01a1!! )'),I('%3\xb1""5#7\xb2.6 &3\xb3""5#7\xb4.* &$;+0#*;+&/\' 8!:\u01a9!! )'),I("%;\u0104/\x87#2F\"\"6F7G/x$;\u0103/o$2F\"\"6F7G/`$;\u0103/W$2F\"\"6F7G/H$;\u0103/?$2F\"\"6F7G/0$;\u0105/'$8):\u01aa) )()'#(('#(''#(&'#(%'#($'#(#'#(\"'#&'#"),I("%;#/>#;#/5$;#/,$;#/#$+$)($'#(#'#(\"'#&'#"),I("%;\u0103/,#;\u0103/#$+\")(\"'#&'#"),I("%;\u0103/5#;\u0103/,$;\u0103/#$+#)(#'#(\"'#&'#"),I("%;q/T#$;m0#*;m&/D$%; /,#;\xf8/#$+\")(\"'#&'#.\" &\"/#$+#)(#'#(\"'#&'#"),I('%2\u01ab""6\u01ab7\u01ac.) &2\u01ad""6\u01ad7\u01ae/w#;0/n$;\u0108/e$$%;B/2#;\u0109.# &;\xa0/#$+")("\'#&\'#0<*%;B/2#;\u0109.# &;\xa0/#$+")("\'#&\'#&/#$+$)($\'#(#\'#("\'#&\'#'),I(";\x99.# &;L"),I("%2\u01af\"\"6\u01af7\u01b0/5#;g&&(g=c,u=[]),u.push(e))}function $(e,t,s){return new S(S.buildMessage(e,t),e,t,s)}function I(e){return e.split("").map((e=>e.charCodeAt(0)-32))}if(t.data={},l=function t(i){const r=a[i];let n=0;const d=[];let l=r.length;const g=[],u=[];let f;for(;;){for(;nc?(l=n+3+r[n+1],n+=3):(l=n+3+r[n+1]+r[n+2],n+=3+r[n+1]);break;case 18:g.push(l),d.push(n+4+r[n+2]+r[n+3]),e.substr(c,o[r[n+1]].length)===o[r[n+1]]?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 19:g.push(l),d.push(n+4+r[n+2]+r[n+3]),e.substr(c,o[r[n+1]].length).toLowerCase()===o[r[n+1]]?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 20:g.push(l),d.push(n+4+r[n+2]+r[n+3]),o[r[n+1]].test(e.charAt(c))?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 21:u.push(e.substr(c,r[n+1])),c+=r[n+1],n+=2;break;case 22:u.push(o[r[n+1]]),c+=o[r[n+1]].length,n+=2;break;case 23:u.push(s),0===p&&E(o[r[n+1]]),n+=2;break;case 24:h=u[u.length-1-r[n+1]],n+=2;break;case 25:h=c,n++;break;case 26:f=r.slice(n+4,n+4+r[n+3]).map((function(e){return u[u.length-1-e]})),u.splice(u.length-r[n+2],r[n+2],o[r[n+1]].apply(null,f)),n+=4+r[n+3];break;case 27:u.push(t(r[n+1])),n+=2;break;case 28:p++,n++;break;case 29:p--,n++;break;default:throw new Error("Invalid opcode: "+r[n]+".")}if(!(g.length>0))break;l=g.pop(),n=d.pop()}return u[0]}(n),l!==s&&c===e.length)return l;throw l!==s&&c=this.headers[e].length)return;const s=this.headers[e][t],i=s.raw;if(s.parsed)return s.parsed;const r=y.parse(i,e.replace(/-/g,"_"));return-1===r?void this.headers[e].splice(t,1):(s.parsed=r,r)}s(e,t=0){return this.parseHeader(e,t)}setHeader(e,t){this.headers[A(e)]=[{raw:t}]}toString(){return this.data}}class P extends H{constructor(){super()}}class x extends H{constructor(){super()}}class k{constructor(e,t,s,i,r,n,o){this.headers={},this.extraHeaders=[],this.options=k.getDefaultOptions(),r&&(this.options=Object.assign(Object.assign({},this.options),r),this.options.optionTags&&this.options.optionTags.length&&(this.options.optionTags=this.options.optionTags.slice()),this.options.routeSet&&this.options.routeSet.length&&(this.options.routeSet=this.options.routeSet.slice())),n&&n.length&&(this.extraHeaders=n.slice()),o&&(this.body={body:o.content,contentType:o.contentType}),this.method=e,this.ruri=t.clone(),this.fromURI=s.clone(),this.fromTag=this.options.fromTag?this.options.fromTag:C(),this.from=k.makeNameAddrHeader(this.fromURI,this.options.fromDisplayName,this.fromTag),this.toURI=i.clone(),this.toTag=this.options.toTag,this.to=k.makeNameAddrHeader(this.toURI,this.options.toDisplayName,this.toTag),this.callId=this.options.callId?this.options.callId:this.options.callIdPrefix+$(15),this.cseq=this.options.cseq,this.setHeader("route",this.options.routeSet),this.setHeader("via",""),this.setHeader("to",this.to.toString()),this.setHeader("from",this.from.toString()),this.setHeader("cseq",this.cseq+" "+this.method),this.setHeader("call-id",this.callId),this.setHeader("max-forwards","70")}static getDefaultOptions(){return{callId:"",callIdPrefix:"",cseq:1,toDisplayName:"",toTag:"",fromDisplayName:"",fromTag:"",forceRport:!1,hackViaTcp:!1,optionTags:["outbound"],routeSet:[],userAgentString:"sip.js",viaHost:""}}static makeNameAddrHeader(e,t,s){const i={};return s&&(i.tag=s),new v(e,t,i)}getHeader(e){const t=this.headers[A(e)];if(t){if(t[0])return t[0]}else{const t=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)if(t.test(e))return e.substring(e.indexOf(":")+1).trim()}}getHeaders(e){const t=[],s=this.headers[A(e)];if(s)for(const e of s)t.push(e);else{const s=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)s.test(e)&&t.push(e.substring(e.indexOf(":")+1).trim())}return t}hasHeader(e){if(this.headers[A(e)])return!0;{const t=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)if(t.test(e))return!0}return!1}setHeader(e,t){this.headers[A(e)]=t instanceof Array?t:[t]}setViaHeader(e,t){this.options.hackViaTcp&&(t="TCP");let s="SIP/2.0/"+t;s+=" "+this.options.viaHost+";branch="+e,this.options.forceRport&&(s+=";rport"),this.setHeader("via",s),this.branch=e}toString(){let e="";e+=this.method+" "+this.ruri.toRaw()+" SIP/2.0\r\n";for(const t in this.headers)if(this.headers[t])for(const s of this.headers[t])e+=t+": "+s+"\r\n";for(const t of this.extraHeaders)e+=t.trim()+"\r\n";return e+="Supported: "+this.options.optionTags.join(", ")+"\r\n",e+="User-Agent: "+this.options.userAgentString+"\r\n",this.body?"string"==typeof this.body?(e+="Content-Length: "+D(this.body)+"\r\n\r\n",e+=this.body):this.body.body&&this.body.contentType?(e+="Content-Type: "+this.body.contentType+"\r\n",e+="Content-Length: "+D(this.body.body)+"\r\n\r\n",e+=this.body.body):e+="Content-Length: 0\r\n\r\n":e+="Content-Length: 0\r\n\r\n",e}}function _(e){return"application/sdp"===e?"session":"render"}function q(e){const t="string"==typeof e?e:e.body,s="string"==typeof e?"application/sdp":e.contentType;return{contentDisposition:_(s),contentType:s,content:t}}function M(e){return!(!e||"string"!=typeof e.content||"string"!=typeof e.contentType||void 0!==e.contentDisposition)||"string"==typeof e.contentDisposition}function N(e){let t,s,i;if(e instanceof P&&e.body){const r=e.parseHeader("Content-Disposition");t=r?r.type:void 0,s=e.parseHeader("Content-Type"),i=e.body}if(e instanceof x&&e.body){const r=e.parseHeader("Content-Disposition");t=r?r.type:void 0,s=e.parseHeader("Content-Type"),i=e.body}if(e instanceof k&&e.body)if(t=e.getHeader("Content-Disposition"),s=e.getHeader("Content-Type"),"string"==typeof e.body){if(!s)throw new Error("Header content type header does not equal body content type.");i=e.body}else{if(s&&s!==e.body.contentType)throw new Error("Header content type header does not equal body content type.");s=e.body.contentType,i=e.body.body}if(M(e)&&(t=e.contentDisposition,s=e.contentType,i=e.content),i){if(s&&!t&&(t=_(s)),!t)throw new Error("Content disposition undefined.");if(!s)throw new Error("Content type undefined.");return{contentDisposition:t,contentType:s,content:i}}}var O,U;!function(e){e.Initial="Initial",e.Early="Early",e.AckWait="AckWait",e.Confirmed="Confirmed",e.Terminated="Terminated"}(O=O||(O={})),function(e){e.Initial="Initial",e.HaveLocalOffer="HaveLocalOffer",e.HaveRemoteOffer="HaveRemoteOffer",e.Stable="Stable",e.Closed="Closed"}(U=U||(U={}));const j=500,F=5e3,L={T1:j,T2:4e3,T4:F,TIMER_B:32e3,TIMER_D:0,TIMER_F:32e3,TIMER_H:32e3,TIMER_I:0,TIMER_J:0,TIMER_K:0,TIMER_L:32e3,TIMER_M:32e3,TIMER_N:32e3,PROVISIONAL_RESPONSE_INTERVAL:6e4};class B extends n{constructor(e){super(e||"Transaction state error.")}}var G;!function(e){e.ACK="ACK",e.BYE="BYE",e.CANCEL="CANCEL",e.INFO="INFO",e.INVITE="INVITE",e.MESSAGE="MESSAGE",e.NOTIFY="NOTIFY",e.OPTIONS="OPTIONS",e.REGISTER="REGISTER",e.UPDATE="UPDATE",e.SUBSCRIBE="SUBSCRIBE",e.PUBLISH="PUBLISH",e.REFER="REFER",e.PRACK="PRACK"}(G=G||(G={}));const V=[G.ACK,G.BYE,G.CANCEL,G.INFO,G.INVITE,G.MESSAGE,G.NOTIFY,G.OPTIONS,G.PRACK,G.REFER,G.REGISTER,G.SUBSCRIBE];class W{constructor(e){this.incomingMessageRequest=e}get request(){return this.incomingMessageRequest.message}accept(e){return this.incomingMessageRequest.accept(e),Promise.resolve()}reject(e){return this.incomingMessageRequest.reject(e),Promise.resolve()}}class K{constructor(e){this.incomingNotifyRequest=e}get request(){return this.incomingNotifyRequest.message}accept(e){return this.incomingNotifyRequest.accept(e),Promise.resolve()}reject(e){return this.incomingNotifyRequest.reject(e),Promise.resolve()}}class Y{constructor(e,t){this.incomingReferRequest=e,this.session=t}get referTo(){const e=this.incomingReferRequest.message.parseHeader("refer-to");if(!(e instanceof v))throw new Error("Failed to parse Refer-To header.");return e}get referredBy(){return this.incomingReferRequest.message.getHeader("referred-by")}get replaces(){const e=this.referTo.uri.getHeader("replaces");return e instanceof Array?e[0]:e}get request(){return this.incomingReferRequest.message}accept(e={statusCode:202}){return this.incomingReferRequest.accept(e),Promise.resolve()}reject(e){return this.incomingReferRequest.reject(e),Promise.resolve()}makeInviter(e){if(this.inviter)return this.inviter;const t=this.referTo.uri.clone();t.clearHeaders();const s=((e=e||{}).extraHeaders||[]).slice(),i=this.replaces;i&&s.push("Replaces: "+decodeURIComponent(i));const r=this.referredBy;return r&&s.push("Referred-By: "+r),e.extraHeaders=s,this.inviter=this.session.userAgent._makeInviter(t,e),this.inviter._referred=this.session,this.session._referral=this.inviter,this.inviter}}var Z,J;!function(e){e.Initial="Initial",e.Establishing="Establishing",e.Established="Established",e.Terminating="Terminating",e.Terminated="Terminated"}(Z=Z||(Z={}));class z{constructor(e,t={}){this.pendingReinvite=!1,this.pendingReinviteAck=!1,this._state=Z.Initial,this.delegate=t.delegate,this._stateEventEmitter=new p,this._userAgent=e}dispose(){switch(this.logger.log(`Session ${this.id} in state ${this._state} is being disposed`),delete this.userAgent._sessions[this.id],this._sessionDescriptionHandler&&this._sessionDescriptionHandler.close(),this.state){case Z.Initial:case Z.Establishing:break;case Z.Established:return new Promise((e=>{this._bye({onAccept:()=>e(),onRedirect:()=>e(),onReject:()=>e()})}));case Z.Terminating:case Z.Terminated:break;default:throw new Error("Unknown state.")}return Promise.resolve()}get assertedIdentity(){return this._assertedIdentity}get dialog(){return this._dialog}get id(){return this._id}get replacee(){return this._replacee}get sessionDescriptionHandler(){return this._sessionDescriptionHandler}get sessionDescriptionHandlerFactory(){return this.userAgent.configuration.sessionDescriptionHandlerFactory}get sessionDescriptionHandlerModifiers(){return this._sessionDescriptionHandlerModifiers||[]}set sessionDescriptionHandlerModifiers(e){this._sessionDescriptionHandlerModifiers=e.slice()}get sessionDescriptionHandlerOptions(){return this._sessionDescriptionHandlerOptions||{}}set sessionDescriptionHandlerOptions(e){this._sessionDescriptionHandlerOptions=Object.assign({},e)}get sessionDescriptionHandlerModifiersReInvite(){return this._sessionDescriptionHandlerModifiersReInvite||[]}set sessionDescriptionHandlerModifiersReInvite(e){this._sessionDescriptionHandlerModifiersReInvite=e.slice()}get sessionDescriptionHandlerOptionsReInvite(){return this._sessionDescriptionHandlerOptionsReInvite||{}}set sessionDescriptionHandlerOptionsReInvite(e){this._sessionDescriptionHandlerOptionsReInvite=Object.assign({},e)}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get userAgent(){return this._userAgent}bye(e={}){let t="Session.bye() may only be called if established session.";switch(this.state){case Z.Initial:"function"==typeof this.cancel?(t+=" However Inviter.invite() has not yet been called.",t+=" Perhaps you should have called Inviter.cancel()?"):"function"==typeof this.reject&&(t+=" However Invitation.accept() has not yet been called.",t+=" Perhaps you should have called Invitation.reject()?");break;case Z.Establishing:"function"==typeof this.cancel?(t+=" However a dialog does not yet exist.",t+=" Perhaps you should have called Inviter.cancel()?"):"function"==typeof this.reject&&(t+=" However Invitation.accept() has not yet been called (or not yet resolved).",t+=" Perhaps you should have called Invitation.reject()?");break;case Z.Established:{const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._bye(t,s)}case Z.Terminating:t+=" However this session is already terminating.","function"==typeof this.cancel?t+=" Perhaps you have already called Inviter.cancel()?":"function"==typeof this.reject&&(t+=" Perhaps you have already called Session.bye()?");break;case Z.Terminated:t+=" However this session is already terminated.";break;default:throw new Error("Unknown state")}return this.logger.error(t),Promise.reject(new Error(`Invalid session state ${this.state}`))}info(e={}){if(this.state!==Z.Established){const e="Session.info() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._info(t,s)}invite(e={}){if(this.logger.log("Session.invite"),this.state!==Z.Established)return Promise.reject(new Error(`Invalid session state ${this.state}`));if(this.pendingReinvite)return Promise.reject(new a("Reinvite in progress. Please wait until complete, then try again."));this.pendingReinvite=!0,e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiersReInvite=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptionsReInvite=e.sessionDescriptionHandlerOptions);const t={onAccept:t=>{const s=N(t.message);if(!s)return this.logger.error("Received 2xx response to re-INVITE without a session description"),this.ackAndBye(t,400,"Missing session description"),this.stateTransition(Z.Terminated),void(this.pendingReinvite=!1);if(e.withoutSdp){const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.setOfferAndGetAnswer(s,i).then((e=>{t.ack({body:e})})).catch((e=>{this.logger.error("Failed to handle offer in 2xx response to re-INVITE"),this.logger.error(e.message),this.state===Z.Terminated?t.ack():(this.ackAndBye(t,488,"Bad Media Description"),this.stateTransition(Z.Terminated))})).then((()=>{this.pendingReinvite=!1,e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))}else{const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.setAnswer(s,i).then((()=>{t.ack()})).catch((e=>{this.logger.error("Failed to handle answer in 2xx response to re-INVITE"),this.logger.error(e.message),this.state!==Z.Terminated?(this.ackAndBye(t,488,"Bad Media Description"),this.stateTransition(Z.Terminated)):t.ack()})).then((()=>{this.pendingReinvite=!1,e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))}},onProgress:e=>{},onRedirect:e=>{},onReject:t=>{this.logger.warn("Received a non-2xx response to re-INVITE"),this.pendingReinvite=!1,e.withoutSdp?e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t):this.rollbackOffer().catch((e=>{if(this.logger.error("Failed to rollback offer on non-2xx response to re-INVITE"),this.logger.error(e.message),this.state!==Z.Terminated){if(!this.dialog)throw new Error("Dialog undefined.");const e=[];e.push("Reason: "+this.getReasonHeaderValue(500,"Internal Server Error")),this.dialog.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated)}})).then((()=>{e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t)}))},onTrying:e=>{}},s=e.requestOptions||{};if(s.extraHeaders=(s.extraHeaders||[]).slice(),s.extraHeaders.push("Allow: "+V.toString()),s.extraHeaders.push("Contact: "+this._contact),e.withoutSdp){if(!this.dialog)throw this.pendingReinvite=!1,new Error("Dialog undefined.");return Promise.resolve(this.dialog.invite(t,s))}const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};return this.getOffer(i).then((e=>{if(!this.dialog)throw this.pendingReinvite=!1,new Error("Dialog undefined.");return s.body=e,this.dialog.invite(t,s)})).catch((e=>{throw this.logger.error(e.message),this.logger.error("Failed to send re-INVITE"),this.pendingReinvite=!1,e}))}message(e={}){if(this.state!==Z.Established){const e="Session.message() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._message(t,s)}refer(e,t={}){if(this.state!==Z.Established){const e="Session.refer() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}if(e instanceof z&&!e.dialog){const e="Session.refer() may only be called with session which is established. You are perhaps attempting to attended transfer to a target for which there is not dialog yet established. Perhaps you are attempting a 'semi-attended' tansfer? Regardless, this is not supported. The recommended approached is to check to see if the target Session is in the Established state before calling refer(); if the state is not Established you may proceed by falling back using a URI as the target (blind transfer).";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const s=t.requestDelegate,i=this.copyRequestOptions(t.requestOptions);return i.extraHeaders=i.extraHeaders?i.extraHeaders.concat(this.referExtraHeaders(this.referToString(e))):this.referExtraHeaders(this.referToString(e)),this._refer(t.onNotify,s,i)}_bye(e,t){if(!this.dialog)return Promise.reject(new Error("Session dialog undefined."));const s=this.dialog;switch(s.sessionState){case O.Initial:case O.Early:throw new Error(`Invalid dialog state ${s.sessionState}`);case O.AckWait:return this.stateTransition(Z.Terminating),new Promise((i=>{s.delegate={onAck:()=>{const r=s.bye(e,t);return this.stateTransition(Z.Terminated),i(r),Promise.resolve()},onAckTimeout:()=>{const r=s.bye(e,t);this.stateTransition(Z.Terminated),i(r)}}}));case O.Confirmed:{const i=s.bye(e,t);return this.stateTransition(Z.Terminated),Promise.resolve(i)}case O.Terminated:throw new Error(`Invalid dialog state ${s.sessionState}`);default:throw new Error("Unrecognized state.")}}_info(e,t){return this.dialog?Promise.resolve(this.dialog.info(e,t)):Promise.reject(new Error("Session dialog undefined."))}_message(e,t){return this.dialog?Promise.resolve(this.dialog.message(e,t)):Promise.reject(new Error("Session dialog undefined."))}_refer(e,t,s){return this.dialog?(this.onNotify=e,Promise.resolve(this.dialog.refer(t,s))):Promise.reject(new Error("Session dialog undefined."))}ackAndBye(e,t,s){e.ack();const i=[];t&&i.push("Reason: "+this.getReasonHeaderValue(t,s)),e.session.bye(void 0,{extraHeaders:i})}onAckRequest(e){if(this.logger.log("Session.onAckRequest"),this.state!==Z.Established&&this.state!==Z.Terminating)return this.logger.error(`ACK received while in state ${this.state}, dropping request`),Promise.resolve();const t=this.dialog;if(!t)throw new Error("Dialog undefined.");const s={sessionDescriptionHandlerOptions:this.pendingReinviteAck?this.sessionDescriptionHandlerOptionsReInvite:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.pendingReinviteAck?this._sessionDescriptionHandlerModifiersReInvite:this._sessionDescriptionHandlerModifiers};if(this.delegate&&this.delegate.onAck){const t=new l(e);this.delegate.onAck(t)}switch(this.pendingReinviteAck=!1,t.signalingState){case U.Initial:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.Stable:{const i=N(e.message);return i?"render"===i.contentDisposition?(this._renderbody=i.content,this._rendertype=i.contentType,Promise.resolve()):"session"!==i.contentDisposition?Promise.resolve():this.setAnswer(i,s).catch((e=>{this.logger.error(e.message);const s=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];t.bye(void 0,{extraHeaders:s}),this.stateTransition(Z.Terminated)})):Promise.resolve()}case U.HaveLocalOffer:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.HaveRemoteOffer:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.Closed:default:throw new Error(`Invalid signaling state ${t.signalingState}.`)}}onByeRequest(e){if(this.logger.log("Session.onByeRequest"),this.state===Z.Established){if(this.delegate&&this.delegate.onBye){const t=new g(e);this.delegate.onBye(t)}else e.accept();this.stateTransition(Z.Terminated)}else this.logger.error(`BYE received while in state ${this.state}, dropping request`)}onInfoRequest(e){if(this.logger.log("Session.onInfoRequest"),this.state===Z.Established)if(this.delegate&&this.delegate.onInfo){const t=new f(e);this.delegate.onInfo(t)}else e.accept();else this.logger.error(`INFO received while in state ${this.state}, dropping request`)}onInviteRequest(e){if(this.logger.log("Session.onInviteRequest"),this.state!==Z.Established)return void this.logger.error(`INVITE received while in state ${this.state}, dropping request`);this.pendingReinviteAck=!0;const t=["Contact: "+this._contact];if(e.message.hasHeader("P-Asserted-Identity")){const t=e.message.getHeader("P-Asserted-Identity");if(!t)throw new Error("Header undefined.");this._assertedIdentity=y.nameAddrHeaderParse(t)}const s={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.generateResponseOfferAnswerInDialog(s).then((s=>{const i=e.accept({statusCode:200,extraHeaders:t,body:s});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,i.message,200)})).catch((t=>{if(this.logger.error(t.message),this.logger.error("Failed to handle to re-INVITE request"),!this.dialog)throw new Error("Dialog undefined.");if(this.logger.error(this.dialog.signalingState),this.dialog.signalingState!==U.Stable)this.rollbackOffer().then((()=>{const t=e.reject({statusCode:488});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,t.message,488)})).catch((t=>{this.logger.error(t.message),this.logger.error("Failed to rollback offer on re-INVITE request");const s=e.reject({statusCode:488});if(this.state!==Z.Terminated){if(!this.dialog)throw new Error("Dialog undefined.");const e=[];e.push("Reason: "+this.getReasonHeaderValue(500,"Internal Server Error")),this.dialog.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated)}this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,s.message,488)}));else{const t=e.reject({statusCode:488});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,t.message,488)}}))}onMessageRequest(e){if(this.logger.log("Session.onMessageRequest"),this.state===Z.Established)if(this.delegate&&this.delegate.onMessage){const t=new W(e);this.delegate.onMessage(t)}else e.accept();else this.logger.error(`MESSAGE received while in state ${this.state}, dropping request`)}onNotifyRequest(e){if(this.logger.log("Session.onNotifyRequest"),this.state===Z.Established)if(this.onNotify){const t=new K(e);this.onNotify(t)}else if(this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else e.accept();else this.logger.error(`NOTIFY received while in state ${this.state}, dropping request`)}onPrackRequest(e){if(this.logger.log("Session.onPrackRequest"),this.state===Z.Established)throw new Error("Unimplemented.");this.logger.error(`PRACK received while in state ${this.state}, dropping request`)}onReferRequest(e){if(this.logger.log("Session.onReferRequest"),this.state!==Z.Established)return void this.logger.error(`REFER received while in state ${this.state}, dropping request`);if(!e.message.hasHeader("refer-to"))return this.logger.warn("Invalid REFER packet. A refer-to header is required. Rejecting."),void e.reject();const t=new Y(e,this);this.delegate&&this.delegate.onRefer?this.delegate.onRefer(t):(this.logger.log("No delegate available to handle REFER, automatically accepting and following."),t.accept().then((()=>t.makeInviter(this._referralInviterOptions).invite())).catch((e=>{this.logger.error(e.message)})))}generateResponseOfferAnswer(e,t){if(this.dialog)return this.generateResponseOfferAnswerInDialog(t);const s=N(e.message);return s&&"session"===s.contentDisposition?this.setOfferAndGetAnswer(s,t):this.getOffer(t)}generateResponseOfferAnswerInDialog(e){if(!this.dialog)throw new Error("Dialog undefined.");switch(this.dialog.signalingState){case U.Initial:return this.getOffer(e);case U.HaveLocalOffer:return Promise.resolve(void 0);case U.HaveRemoteOffer:if(!this.dialog.offer)throw new Error(`Session offer undefined in signaling state ${this.dialog.signalingState}.`);return this.setOfferAndGetAnswer(this.dialog.offer,e);case U.Stable:return this.state!==Z.Established?Promise.resolve(void 0):this.getOffer(e);case U.Closed:default:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`)}}getOffer(e){const t=this.setupSessionDescriptionHandler(),s=e.sessionDescriptionHandlerOptions,i=e.sessionDescriptionHandlerModifiers;try{return t.getDescription(s,i).then((e=>q(e))).catch((e=>{this.logger.error("Session.getOffer: SDH getDescription rejected...");const t=e instanceof Error?e:new Error("Session.getOffer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.getOffer: SDH getDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}rollbackOffer(){const e=this.setupSessionDescriptionHandler();if(void 0===e.rollbackDescription)return Promise.resolve();try{return e.rollbackDescription().catch((e=>{this.logger.error("Session.rollbackOffer: SDH rollbackDescription rejected...");const t=e instanceof Error?e:new Error("Session.rollbackOffer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.rollbackOffer: SDH rollbackDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setAnswer(e,t){const s=this.setupSessionDescriptionHandler(),i=t.sessionDescriptionHandlerOptions,r=t.sessionDescriptionHandlerModifiers;try{if(!s.hasDescription(e.contentType))return Promise.reject(new o)}catch(e){this.logger.error("Session.setAnswer: SDH hasDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}try{return s.setDescription(e.content,i,r).catch((e=>{this.logger.error("Session.setAnswer: SDH setDescription rejected...");const t=e instanceof Error?e:new Error("Session.setAnswer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.setAnswer: SDH setDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setOfferAndGetAnswer(e,t){const s=this.setupSessionDescriptionHandler(),i=t.sessionDescriptionHandlerOptions,r=t.sessionDescriptionHandlerModifiers;try{if(!s.hasDescription(e.contentType))return Promise.reject(new o)}catch(e){this.logger.error("Session.setOfferAndGetAnswer: SDH hasDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}try{return s.setDescription(e.content,i,r).then((()=>s.getDescription(i,r))).then((e=>q(e))).catch((e=>{this.logger.error("Session.setOfferAndGetAnswer: SDH setDescription or getDescription rejected...");const t=e instanceof Error?e:new Error("Session.setOfferAndGetAnswer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.setOfferAndGetAnswer: SDH setDescription or getDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setSessionDescriptionHandler(e){if(this._sessionDescriptionHandler)throw new Error("Session description handler defined.");this._sessionDescriptionHandler=e}setupSessionDescriptionHandler(){var e;return this._sessionDescriptionHandler||(this._sessionDescriptionHandler=this.sessionDescriptionHandlerFactory(this,this.userAgent.configuration.sessionDescriptionHandlerFactoryOptions),(null===(e=this.delegate)||void 0===e?void 0:e.onSessionDescriptionHandler)&&this.delegate.onSessionDescriptionHandler(this._sessionDescriptionHandler,!1)),this._sessionDescriptionHandler}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case Z.Initial:e!==Z.Establishing&&e!==Z.Established&&e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Establishing:e!==Z.Established&&e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Established:e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Terminating:e!==Z.Terminated&&t();break;case Z.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Session ${this.id} transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===Z.Terminated&&this.dispose()}copyRequestOptions(e={}){return{extraHeaders:e.extraHeaders?e.extraHeaders.slice():void 0,body:e.body?{contentDisposition:e.body.contentDisposition||"render",contentType:e.body.contentType||"text/plain",content:e.body.content||""}:void 0}}getReasonHeaderValue(e,t){const s=e;let i=I(e);return!i&&t&&(i=t),"SIP;cause="+s+';text="'+i+'"'}referExtraHeaders(e){const t=[];return t.push("Referred-By: <"+this.userAgent.configuration.uri+">"),t.push("Contact: "+this._contact),t.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),t.push("Refer-To: "+e),t}referToString(e){let t;if(e instanceof w)t=e.toString();else{if(!e.dialog)throw new Error("Dialog undefined.");const s=e.remoteIdentity.friendlyName,i=e.dialog.remoteTarget.toString(),r=e.dialog.callId,n=e.dialog.remoteTag,o=e.dialog.localTag;t=`"${s}" <${i}?Replaces=${encodeURIComponent(`${r};to-tag=${n};from-tag=${o}`)}>`}return t}}!function(e){e.Required="Required",e.Supported="Supported",e.Unsupported="Unsupported"}(J=J||(J={}));const X={"100rel":!0,199:!0,answermode:!0,"early-session":!0,eventlist:!0,explicitsub:!0,"from-change":!0,"geolocation-http":!0,"geolocation-sip":!0,gin:!0,gruu:!0,histinfo:!0,ice:!0,join:!0,"multiple-refer":!0,norefersub:!0,nosub:!0,outbound:!0,path:!0,policy:!0,precondition:!0,pref:!0,privacy:!0,"recipient-list-invite":!0,"recipient-list-message":!0,"recipient-list-subscribe":!0,replaces:!0,"resource-priority":!0,"sdp-anat":!0,"sec-agree":!0,tdialog:!0,timer:!0,uui:!0};class Q extends z{constructor(e,t){super(e),this.incomingInviteRequest=t,this.disposed=!1,this.expiresTimer=void 0,this.isCanceled=!1,this.rel100="none",this.rseq=Math.floor(1e4*Math.random()),this.userNoAnswerTimer=void 0,this.waitingForPrack=!1,this.logger=e.getLogger("sip.Invitation");const s=this.incomingInviteRequest.message,i=s.getHeader("require");i&&i.toLowerCase().includes("100rel")&&(this.rel100="required");const r=s.getHeader("supported");if(r&&r.toLowerCase().includes("100rel")&&(this.rel100="supported"),s.toTag=t.toTag,"string"!=typeof s.toTag)throw new TypeError("toTag should have been a string.");if(this.userNoAnswerTimer=setTimeout((()=>{t.reject({statusCode:480}),this.stateTransition(Z.Terminated)}),this.userAgent.configuration.noAnswerTimeout?1e3*this.userAgent.configuration.noAnswerTimeout:6e4),s.hasHeader("expires")){const e=1e3*Number(s.getHeader("expires")||0);this.expiresTimer=setTimeout((()=>{this.state===Z.Initial&&(t.reject({statusCode:487}),this.stateTransition(Z.Terminated))}),e)}const n=this.request.getHeader("P-Asserted-Identity");n&&(this._assertedIdentity=y.nameAddrHeaderParse(n)),this._contact=this.userAgent.contact.toString();const o=s.parseHeader("Content-Disposition");o&&"render"===o.type&&(this._renderbody=s.body,this._rendertype=s.getHeader("Content-Type")),this._id=s.callId+s.fromTag,this.userAgent._sessions[this._id]=this}dispose(){if(this.disposed)return Promise.resolve();switch(this.disposed=!0,this.expiresTimer&&(clearTimeout(this.expiresTimer),this.expiresTimer=void 0),this.userNoAnswerTimer&&(clearTimeout(this.userNoAnswerTimer),this.userNoAnswerTimer=void 0),this.prackNeverArrived(),this.state){case Z.Initial:case Z.Establishing:return this.reject().then((()=>super.dispose()));case Z.Established:case Z.Terminating:case Z.Terminated:return super.dispose();default:throw new Error("Unknown state.")}}get autoSendAnInitialProvisionalResponse(){return"required"!==this.rel100&&this.userAgent.configuration.sendInitialProvisionalResponse}get body(){return this.incomingInviteRequest.message.body}get localIdentity(){return this.request.to}get remoteIdentity(){return this.request.from}get request(){return this.incomingInviteRequest.message}accept(e={}){if(this.logger.log("Invitation.accept"),this.state!==Z.Initial){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}return e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),this.stateTransition(Z.Establishing),this.sendAccept(e).then((({message:e,session:t})=>{t.delegate={onAck:e=>this.onAckRequest(e),onAckTimeout:()=>this.onAckTimeout(),onBye:e=>this.onByeRequest(e),onInfo:e=>this.onInfoRequest(e),onInvite:e=>this.onInviteRequest(e),onMessage:e=>this.onMessageRequest(e),onNotify:e=>this.onNotifyRequest(e),onPrack:e=>this.onPrackRequest(e),onRefer:e=>this.onReferRequest(e)},this._dialog=t,this.stateTransition(Z.Established),this._replacee&&this._replacee._bye()})).catch((e=>this.handleResponseError(e)))}progress(e={}){if(this.logger.log("Invitation.progress"),this.state!==Z.Initial){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}const t=e.statusCode||180;if(t<100||t>199)throw new TypeError("Invalid statusCode: "+t);return e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),this.waitingForPrack?(this.logger.warn("Unexpected call for progress while waiting for prack, ignoring"),Promise.resolve()):100===e.statusCode?this.sendProgressTrying().then((()=>{})).catch((e=>this.handleResponseError(e))):"required"===this.rel100||"supported"===this.rel100&&e.rel100||"supported"===this.rel100&&this.userAgent.configuration.sipExtension100rel===J.Required?this.sendProgressReliableWaitForPrack(e).then((()=>{})).catch((e=>this.handleResponseError(e))):this.sendProgress(e).then((()=>{})).catch((e=>this.handleResponseError(e)))}reject(e={}){if(this.logger.log("Invitation.reject"),this.state!==Z.Initial&&this.state!==Z.Establishing){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}const t=e.statusCode||480,s=e.reasonPhrase?e.reasonPhrase:I(t),i=e.extraHeaders||[];if(t<300||t>699)throw new TypeError("Invalid statusCode: "+t);const r=e.body?q(e.body):void 0;return t<400?this.incomingInviteRequest.redirect([],{statusCode:t,reasonPhrase:s,extraHeaders:i,body:r}):this.incomingInviteRequest.reject({statusCode:t,reasonPhrase:s,extraHeaders:i,body:r}),this.stateTransition(Z.Terminated),Promise.resolve()}_onCancel(e){if(this.logger.log("Invitation._onCancel"),this.state===Z.Initial||this.state===Z.Establishing){if(this.delegate&&this.delegate.onCancel){const t=new u(e);this.delegate.onCancel(t)}this.isCanceled=!0,this.incomingInviteRequest.reject({statusCode:487}),this.stateTransition(Z.Terminated)}else this.logger.error(`CANCEL received while in state ${this.state}, dropping request`)}handlePrackOfferAnswer(e){if(!this.dialog)throw new Error("Dialog undefined.");const t=N(e.message);if(!t||"session"!==t.contentDisposition)return Promise.resolve(void 0);const s={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers};switch(this.dialog.signalingState){case U.Initial:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);case U.Stable:return this.setAnswer(t,s).then((()=>{}));case U.HaveLocalOffer:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);case U.HaveRemoteOffer:return this.setOfferAndGetAnswer(t,s);case U.Closed:default:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`)}}handleResponseError(e){let t=480;if(e instanceof Error?this.logger.error(e.message):this.logger.error(e),e instanceof o?(this.logger.error("A session description handler occurred while sending response (content type unsupported"),t=415):e instanceof c?this.logger.error("A session description handler occurred while sending response"):e instanceof h?this.logger.error("Session ended before response could be formulated and sent (while waiting for PRACK)"):e instanceof B&&this.logger.error("Session changed state before response could be formulated and sent"),this.state===Z.Initial||this.state===Z.Establishing)try{this.incomingInviteRequest.reject({statusCode:t}),this.stateTransition(Z.Terminated)}catch(e){throw this.logger.error("An error occurred attempting to reject the request while handling another error"),e}if(!this.isCanceled)throw e;this.logger.warn("An error occurred while attempting to formulate and send a response to an incoming INVITE. However a CANCEL was received and processed while doing so which can (and often does) result in errors occurring as the session terminates in the meantime. Said error is being ignored.")}onAckTimeout(){if(this.logger.log("Invitation.onAckTimeout"),!this.dialog)throw new Error("Dialog undefined.");this.logger.log("No ACK received for an extended period of time, terminating session"),this.dialog.bye(),this.stateTransition(Z.Terminated)}sendAccept(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.extraHeaders||[];return this.waitingForPrack?this.waitForArrivalOfPrack().then((()=>clearTimeout(this.userNoAnswerTimer))).then((()=>this.generateResponseOfferAnswer(this.incomingInviteRequest,t))).then((e=>this.incomingInviteRequest.accept({statusCode:200,body:e,extraHeaders:s}))):(clearTimeout(this.userNoAnswerTimer),this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>this.incomingInviteRequest.accept({statusCode:200,body:e,extraHeaders:s}))))}sendProgress(e={}){const t=e.statusCode||180,s=e.reasonPhrase,i=(e.extraHeaders||[]).slice(),r=e.body?q(e.body):void 0;if(183===t&&!r)return this.sendProgressWithSDP(e);try{const e=this.incomingInviteRequest.progress({statusCode:t,reasonPhrase:s,extraHeaders:i,body:r});return this._dialog=e.session,Promise.resolve(e)}catch(e){return Promise.reject(e)}}sendProgressWithSDP(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.statusCode||183,i=e.reasonPhrase,r=(e.extraHeaders||[]).slice();return this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:e}))).then((e=>(this._dialog=e.session,e)))}sendProgressReliable(e={}){return e.extraHeaders=(e.extraHeaders||[]).slice(),e.extraHeaders.push("Require: 100rel"),e.extraHeaders.push("RSeq: "+Math.floor(1e4*Math.random())),this.sendProgressWithSDP(e)}sendProgressReliableWaitForPrack(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.statusCode||183,i=e.reasonPhrase,r=(e.extraHeaders||[]).slice();let n;return r.push("Require: 100rel"),r.push("RSeq: "+this.rseq++),new Promise(((e,o)=>{this.waitingForPrack=!0,this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>(n=e,this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:n})))).then((t=>{let a,c;this._dialog=t.session,t.session.delegate={onPrack:s=>{a=s,clearTimeout(d),clearTimeout(u),this.waitingForPrack&&(this.waitingForPrack=!1,this.handlePrackOfferAnswer(a).then((s=>{try{c=a.accept({statusCode:200,body:s}),this.prackArrived(),e({prackRequest:a,prackResponse:c,progressResponse:t})}catch(e){o(e)}})).catch((e=>o(e))))}};const d=setTimeout((()=>{this.waitingForPrack&&(this.waitingForPrack=!1,this.logger.warn("No PRACK received, rejecting INVITE."),clearTimeout(u),this.reject({statusCode:504}).then((()=>o(new h))).catch((e=>o(e))))}),64*L.T1),l=()=>{try{this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:n})}catch(e){return this.waitingForPrack=!1,void o(e)}u=setTimeout(l,g*=2)};let g=L.T1,u=setTimeout(l,g)})).catch((e=>{this.waitingForPrack=!1,o(e)}))}))}sendProgressTrying(){try{const e=this.incomingInviteRequest.trying();return Promise.resolve(e)}catch(e){return Promise.reject(e)}}waitForArrivalOfPrack(){if(this.waitingForPrackPromise)throw new Error("Already waiting for PRACK");return this.waitingForPrackPromise=new Promise(((e,t)=>{this.waitingForPrackResolve=e,this.waitingForPrackReject=t})),this.waitingForPrackPromise}prackArrived(){this.waitingForPrackResolve&&this.waitingForPrackResolve(),this.waitingForPrackPromise=void 0,this.waitingForPrackResolve=void 0,this.waitingForPrackReject=void 0}prackNeverArrived(){this.waitingForPrackReject&&this.waitingForPrackReject(new h),this.waitingForPrackPromise=void 0,this.waitingForPrackResolve=void 0,this.waitingForPrackReject=void 0}}class ee extends z{constructor(e,t,s={}){super(e,s),this.disposed=!1,this.earlyMedia=!1,this.earlyMediaSessionDescriptionHandlers=new Map,this.isCanceled=!1,this.inviteWithoutSdp=!1,this.logger=e.getLogger("sip.Inviter"),this.earlyMedia=void 0!==s.earlyMedia?s.earlyMedia:this.earlyMedia,this.fromTag=C(),this.inviteWithoutSdp=void 0!==s.inviteWithoutSdp?s.inviteWithoutSdp:this.inviteWithoutSdp;const i=Object.assign({},s);i.params=Object.assign({},s.params);const r=s.anonymous||!1,n=e.contact.toString({anonymous:r,outbound:r?!e.contact.tempGruu:!e.contact.pubGruu});r&&e.configuration.uri&&(i.params.fromDisplayName="Anonymous",i.params.fromUri="sip:anonymous@anonymous.invalid");let o=e.userAgentCore.configuration.aor;if(i.params.fromUri&&(o="string"==typeof i.params.fromUri?y.URIParse(i.params.fromUri):i.params.fromUri),!o)throw new TypeError("Invalid from URI: "+i.params.fromUri);let a=t;if(i.params.toUri&&(a="string"==typeof i.params.toUri?y.URIParse(i.params.toUri):i.params.toUri),!a)throw new TypeError("Invalid to URI: "+i.params.toUri);const c=Object.assign({},i.params);c.fromTag=this.fromTag;const h=(i.extraHeaders||[]).slice();r&&e.configuration.uri&&(h.push("P-Preferred-Identity: "+e.configuration.uri.toString()),h.push("Privacy: id")),h.push("Contact: "+n),h.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),e.configuration.sipExtension100rel===J.Required&&h.push("Require: 100rel"),e.configuration.sipExtensionReplaces===J.Required&&h.push("Require: replaces"),i.extraHeaders=h;this.outgoingRequestMessage=e.userAgentCore.makeOutgoingRequestMessage(G.INVITE,t,o,a,c,h,undefined),this._contact=n,this._referralInviterOptions=i,this._renderbody=s.renderbody,this._rendertype=s.rendertype,s.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=s.sessionDescriptionHandlerModifiers),s.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=s.sessionDescriptionHandlerOptions),s.sessionDescriptionHandlerModifiersReInvite&&(this.sessionDescriptionHandlerModifiersReInvite=s.sessionDescriptionHandlerModifiersReInvite),s.sessionDescriptionHandlerOptionsReInvite&&(this.sessionDescriptionHandlerOptionsReInvite=s.sessionDescriptionHandlerOptionsReInvite),this._id=this.outgoingRequestMessage.callId+this.fromTag,this.userAgent._sessions[this._id]=this}dispose(){if(this.disposed)return Promise.resolve();switch(this.disposed=!0,this.disposeEarlyMedia(),this.state){case Z.Initial:case Z.Establishing:return this.cancel().then((()=>super.dispose()));case Z.Established:case Z.Terminating:case Z.Terminated:return super.dispose();default:throw new Error("Unknown state.")}}get body(){return this.outgoingRequestMessage.body}get localIdentity(){return this.outgoingRequestMessage.from}get remoteIdentity(){return this.outgoingRequestMessage.to}get request(){return this.outgoingRequestMessage}cancel(e={}){if(this.logger.log("Inviter.cancel"),this.state!==Z.Initial&&this.state!==Z.Establishing){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}if(this.isCanceled=!0,this.stateTransition(Z.Terminating),this.outgoingInviteRequest){let t;e.statusCode&&e.reasonPhrase&&(t=function(e,t){if(e&&e<200||e>699)throw new TypeError("Invalid statusCode: "+e);if(e)return"SIP;cause="+e+';text="'+(I(e)||t)+'"'}(e.statusCode,e.reasonPhrase)),this.outgoingInviteRequest.cancel(t,e)}else this.logger.warn("Canceled session before INVITE was sent"),this.stateTransition(Z.Terminated);return Promise.resolve()}invite(e={}){if(this.logger.log("Inviter.invite"),this.state!==Z.Initial)return super.invite(e);if(e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),e.withoutSdp||this.inviteWithoutSdp)return this._renderbody&&this._rendertype&&(this.outgoingRequestMessage.body={contentType:this._rendertype,body:this._renderbody}),this.stateTransition(Z.Establishing),Promise.resolve(this.sendInvite(e));const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.getOffer(t).then((t=>(this.outgoingRequestMessage.body={body:t.content,contentType:t.contentType},this.stateTransition(Z.Establishing),this.sendInvite(e)))).catch((e=>{throw this.logger.log(e.message),this.state!==Z.Terminated&&this.stateTransition(Z.Terminated),e}))}sendInvite(e={}){return this.outgoingInviteRequest=this.userAgent.userAgentCore.invite(this.outgoingRequestMessage,{onAccept:t=>this.dialog?(this.logger.log("Additional confirmed dialog, sending ACK and BYE"),void this.ackAndBye(t)):this.isCanceled?(this.logger.log("Canceled session accepted, sending ACK and BYE"),this.ackAndBye(t),void this.stateTransition(Z.Terminated)):(this.notifyReferer(t),void this.onAccept(t).then((()=>{this.disposeEarlyMedia()})).catch((()=>{this.disposeEarlyMedia()})).then((()=>{e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))),onProgress:t=>{this.isCanceled||(this.notifyReferer(t),this.onProgress(t).catch((()=>{this.disposeEarlyMedia()})).then((()=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)})))},onRedirect:t=>{this.notifyReferer(t),this.onRedirect(t),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t)},onReject:t=>{this.notifyReferer(t),this.onReject(t),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t)},onTrying:t=>{this.notifyReferer(t),this.onTrying(t),e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}}),this.outgoingInviteRequest}disposeEarlyMedia(){this.earlyMediaSessionDescriptionHandlers.forEach((e=>{e.close()})),this.earlyMediaSessionDescriptionHandlers.clear()}notifyReferer(e){if(!this._referred)return;if(!(this._referred instanceof z))throw new Error("Referred session not instance of session");if(!this._referred.dialog)return;if(!e.message.statusCode)throw new Error("Status code undefined.");if(!e.message.reasonPhrase)throw new Error("Reason phrase undefined.");const t=`SIP/2.0 ${e.message.statusCode} ${e.message.reasonPhrase}`.trim();this._referred.dialog.notify(void 0,{extraHeaders:["Event: refer","Subscription-State: terminated"],body:{contentDisposition:"render",contentType:"message/sipfrag",content:t}}).delegate={onReject:()=>{this._referred=void 0}}}onAccept(e){if(this.logger.log("Inviter.onAccept"),this.state!==Z.Establishing)return this.logger.error(`Accept received while in state ${this.state}, dropping response`),Promise.reject(new Error(`Invalid session state ${this.state}`));const t=e.message,s=e.session;switch(t.hasHeader("P-Asserted-Identity")&&(this._assertedIdentity=y.nameAddrHeaderParse(t.getHeader("P-Asserted-Identity"))),s.delegate={onAck:e=>this.onAckRequest(e),onBye:e=>this.onByeRequest(e),onInfo:e=>this.onInfoRequest(e),onInvite:e=>this.onInviteRequest(e),onMessage:e=>this.onMessageRequest(e),onNotify:e=>this.onNotifyRequest(e),onPrack:e=>this.onPrackRequest(e),onRefer:e=>this.onReferRequest(e)},this._dialog=s,s.signalingState){case U.Initial:case U.HaveLocalOffer:return this.logger.error("Received 2xx response to INVITE without a session description"),this.ackAndBye(e,400,"Missing session description"),this.stateTransition(Z.Terminated),Promise.reject(new Error("Bad Media Description"));case U.HaveRemoteOffer:{if(!this._dialog.offer)throw new Error(`Session offer undefined in signaling state ${this._dialog.signalingState}.`);const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setOfferAndGetAnswer(this._dialog.offer,t).then((t=>{e.ack({body:t}),this.stateTransition(Z.Established)})).catch((t=>{throw this.ackAndBye(e,488,"Invalid session description"),this.stateTransition(Z.Terminated),t}))}case U.Stable:{if(this.earlyMediaSessionDescriptionHandlers.size>0){const t=this.earlyMediaSessionDescriptionHandlers.get(s.id);if(!t)throw new Error("Session description handler undefined.");return this.setSessionDescriptionHandler(t),this.earlyMediaSessionDescriptionHandlers.delete(s.id),e.ack(),this.stateTransition(Z.Established),Promise.resolve()}if(this.earlyMediaDialog){if(this.earlyMediaDialog!==s){if(this.earlyMedia){const e="You have set the 'earlyMedia' option to 'true' which requires that your INVITE requests do not fork and yet this INVITE request did in fact fork. Consequentially and not surprisingly the end point which accepted the INVITE (confirmed dialog) does not match the end point with which early media has been setup (early dialog) and thus this session is unable to proceed. In accordance with the SIP specifications, the SIP servers your end point is connected to determine if an INVITE forks and the forking behavior of those servers cannot be controlled by this library. If you wish to use early media with this library you must configure those servers accordingly. Alternatively you may set the 'earlyMedia' to 'false' which will allow this library to function with any INVITE requests which do fork.";this.logger.error(e)}const t=new Error("Early media dialog does not equal confirmed dialog, terminating session");return this.logger.error(t.message),this.ackAndBye(e,488,"Not Acceptable Here"),this.stateTransition(Z.Terminated),Promise.reject(t)}return e.ack(),this.stateTransition(Z.Established),Promise.resolve()}const t=s.answer;if(!t)throw new Error("Answer is undefined.");const i={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setAnswer(t,i).then((()=>{let t;this._renderbody&&this._rendertype&&(t={body:{contentDisposition:"render",contentType:this._rendertype,content:this._renderbody}}),e.ack(t),this.stateTransition(Z.Established)})).catch((t=>{throw this.logger.error(t.message),this.ackAndBye(e,488,"Not Acceptable Here"),this.stateTransition(Z.Terminated),t}))}case U.Closed:return Promise.reject(new Error("Terminated."));default:throw new Error("Unknown session signaling state.")}}onProgress(e){var t;if(this.logger.log("Inviter.onProgress"),this.state!==Z.Establishing)return this.logger.error(`Progress received while in state ${this.state}, dropping response`),Promise.reject(new Error(`Invalid session state ${this.state}`));if(!this.outgoingInviteRequest)throw new Error("Outgoing INVITE request undefined.");const s=e.message,i=e.session;s.hasHeader("P-Asserted-Identity")&&(this._assertedIdentity=y.nameAddrHeaderParse(s.getHeader("P-Asserted-Identity")));const r=s.getHeader("require"),n=s.getHeader("rseq"),o=!!(r&&r.includes("100rel")&&n?Number(n):void 0),a=[];switch(o&&a.push("RAck: "+s.getHeader("rseq")+" "+s.getHeader("cseq")),i.signalingState){case U.Initial:return o&&(this.logger.warn("First reliable provisional response received MUST contain an offer when INVITE does not contain an offer."),e.prack({extraHeaders:a})),Promise.resolve();case U.HaveLocalOffer:return o&&e.prack({extraHeaders:a}),Promise.resolve();case U.HaveRemoteOffer:if(!o)return this.logger.warn("Non-reliable provisional response MUST NOT contain an initial offer, discarding response."),Promise.resolve();{const r=this.sessionDescriptionHandlerFactory(this,this.userAgent.configuration.sessionDescriptionHandlerFactoryOptions||{});return(null===(t=this.delegate)||void 0===t?void 0:t.onSessionDescriptionHandler)&&this.delegate.onSessionDescriptionHandler(r,!0),this.earlyMediaSessionDescriptionHandlers.set(i.id,r),r.setDescription(s.body,this.sessionDescriptionHandlerOptions,this.sessionDescriptionHandlerModifiers).then((()=>r.getDescription(this.sessionDescriptionHandlerOptions,this.sessionDescriptionHandlerModifiers))).then((t=>{const s={contentDisposition:"session",contentType:t.contentType,content:t.body};e.prack({extraHeaders:a,body:s})})).catch((e=>{throw this.stateTransition(Z.Terminated),e}))}case U.Stable:if(o&&e.prack({extraHeaders:a}),this.earlyMedia&&!this.earlyMediaDialog){this.earlyMediaDialog=i;const e=i.answer;if(!e)throw new Error("Answer is undefined.");const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setAnswer(e,t).catch((e=>{throw this.stateTransition(Z.Terminated),e}))}return Promise.resolve();case U.Closed:return Promise.reject(new Error("Terminated."));default:throw new Error("Unknown session signaling state.")}}onRedirect(e){this.logger.log("Inviter.onRedirect"),this.state===Z.Establishing||this.state===Z.Terminating?this.stateTransition(Z.Terminated):this.logger.error(`Redirect received while in state ${this.state}, dropping response`)}onReject(e){this.logger.log("Inviter.onReject"),this.state===Z.Establishing||this.state===Z.Terminating?this.stateTransition(Z.Terminated):this.logger.error(`Reject received while in state ${this.state}, dropping response`)}onTrying(e){this.logger.log("Inviter.onTrying"),this.state===Z.Establishing||this.logger.error(`Trying received while in state ${this.state}, dropping response`)}}class te{constructor(e,t,s,i="text/plain",r={}){this.logger=e.getLogger("sip.Messager"),r.params=r.params||{};let n=e.userAgentCore.configuration.aor;if(r.params.fromUri&&(n="string"==typeof r.params.fromUri?y.URIParse(r.params.fromUri):r.params.fromUri),!n)throw new TypeError("Invalid from URI: "+r.params.fromUri);let o=t;if(r.params.toUri&&(o="string"==typeof r.params.toUri?y.URIParse(r.params.toUri):r.params.toUri),!o)throw new TypeError("Invalid to URI: "+r.params.toUri);const a=r.params?Object.assign({},r.params):{},c=(r.extraHeaders||[]).slice(),h={contentDisposition:"render",contentType:i,content:s};this.request=e.userAgentCore.makeOutgoingRequestMessage(G.MESSAGE,t,n,o,a,c,h),this.userAgent=e}message(e={}){return this.userAgent.userAgentCore.request(this.request,e.requestDelegate),Promise.resolve()}}var se,ie,re,ne,oe,ae,ce,he,de;!function(e){e.Initial="Initial",e.Published="Published",e.Unpublished="Unpublished",e.Terminated="Terminated"}(se=se||(se={}));class le{constructor(e,t,s,i={}){this.disposed=!1,this._state=se.Initial,this._stateEventEmitter=new p,this.userAgent=e,i.extraHeaders=(i.extraHeaders||[]).slice(),i.contentType=i.contentType||"text/plain","number"!=typeof i.expires||i.expires%1!=0?i.expires=3600:i.expires=Number(i.expires),"boolean"!=typeof i.unpublishOnClose&&(i.unpublishOnClose=!0),this.target=t,this.event=s,this.options=i,this.pubRequestExpires=i.expires,this.logger=e.getLogger("sip.Publisher");const r=i.params||{},n=r.fromUri?r.fromUri:e.userAgentCore.configuration.aor,o=r.toUri?r.toUri:t;let a;if(i.body&&i.contentType){a={contentDisposition:"render",contentType:i.contentType,content:i.body}}const c=(i.extraHeaders||[]).slice();this.request=e.userAgentCore.makeOutgoingRequestMessage(G.PUBLISH,t,n,o,r,c,a),this.id=this.target.toString()+":"+this.event,this.userAgent._publishers[this.id]=this}dispose(){return this.disposed?Promise.resolve():(this.disposed=!0,this.logger.log(`Publisher ${this.id} in state ${this.state} is being disposed`),delete this.userAgent._publishers[this.id],this.options.unpublishOnClose&&this.state===se.Published?this.unpublish():(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestExpires=0,this.pubRequestEtag=void 0,Promise.resolve()))}get state(){return this._state}get stateChange(){return this._stateEventEmitter}publish(e,t={}){if(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.options.body=e,this.pubRequestBody=this.options.body,0===this.pubRequestExpires){if(void 0===this.options.expires)throw new Error("Expires undefined.");this.pubRequestExpires=this.options.expires,this.pubRequestEtag=void 0}return this.sendPublishRequest(),Promise.resolve()}unpublish(e={}){return this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestExpires=0,void 0!==this.pubRequestEtag&&this.sendPublishRequest(),Promise.resolve()}receiveResponse(e){const t=e.statusCode||0;switch(!0){case/^1[0-9]{2}$/.test(t.toString()):break;case/^2[0-9]{2}$/.test(t.toString()):if(e.hasHeader("SIP-ETag")?this.pubRequestEtag=e.getHeader("SIP-ETag"):this.logger.warn("SIP-ETag header missing in a 200-class response to PUBLISH"),e.hasHeader("Expires")){const t=Number(e.getHeader("Expires"));"number"==typeof t&&t>=0&&t<=this.pubRequestExpires?this.pubRequestExpires=t:this.logger.warn("Bad Expires header in a 200-class response to PUBLISH")}else this.logger.warn("Expires header missing in a 200-class response to PUBLISH");0!==this.pubRequestExpires?(this.publishRefreshTimer=setTimeout((()=>this.refreshRequest()),900*this.pubRequestExpires),this._state!==se.Published&&this.stateTransition(se.Published)):this.stateTransition(se.Unpublished);break;case/^412$/.test(t.toString()):if(void 0!==this.pubRequestEtag&&0!==this.pubRequestExpires){if(this.logger.warn("412 response to PUBLISH, recovering"),this.pubRequestEtag=void 0,void 0===this.options.body)throw new Error("Body undefined.");this.publish(this.options.body)}else this.logger.warn("412 response to PUBLISH, recovery failed"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated);break;case/^423$/.test(t.toString()):if(0!==this.pubRequestExpires&&e.hasHeader("Min-Expires")){const t=Number(e.getHeader("Min-Expires"));if("number"==typeof t||t>this.pubRequestExpires){if(this.logger.warn("423 code in response to PUBLISH, adjusting the Expires value and trying to recover"),this.pubRequestExpires=t,void 0===this.options.body)throw new Error("Body undefined.");this.publish(this.options.body)}else this.logger.warn("Bad 423 response Min-Expires header received for PUBLISH"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated)}else this.logger.warn("423 response to PUBLISH, recovery failed"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated);break;default:this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated)}0===this.pubRequestExpires&&(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestEtag=void 0)}send(){return this.userAgent.userAgentCore.publish(this.request,{onAccept:e=>this.receiveResponse(e.message),onProgress:e=>this.receiveResponse(e.message),onRedirect:e=>this.receiveResponse(e.message),onReject:e=>this.receiveResponse(e.message),onTrying:e=>this.receiveResponse(e.message)})}refreshRequest(){if(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,void 0===this.pubRequestEtag)throw new Error("Etag undefined");if(0===this.pubRequestExpires)throw new Error("Expires zero");this.sendPublishRequest()}sendPublishRequest(){const e=Object.assign({},this.options);e.extraHeaders=(this.options.extraHeaders||[]).slice(),e.extraHeaders.push("Event: "+this.event),e.extraHeaders.push("Expires: "+this.pubRequestExpires),void 0!==this.pubRequestEtag&&e.extraHeaders.push("SIP-If-Match: "+this.pubRequestEtag);const t=this.target,s=this.options.params||{};let i,r;if(void 0!==this.pubRequestBody){if(void 0===this.options.contentType)throw new Error("Content type undefined.");i={body:this.pubRequestBody,contentType:this.options.contentType}}return i&&(r=q(i)),this.request=this.userAgent.userAgentCore.makeOutgoingRequestMessage(G.PUBLISH,t,s.fromUri?s.fromUri:this.userAgent.userAgentCore.configuration.aor,s.toUri?s.toUri:this.target,s,e.extraHeaders,r),this.send()}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case se.Initial:e!==se.Published&&e!==se.Unpublished&&e!==se.Terminated&&t();break;case se.Published:e!==se.Unpublished&&e!==se.Terminated&&t();break;case se.Unpublished:e!==se.Published&&e!==se.Terminated&&t();break;case se.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Publication transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===se.Terminated&&this.dispose()}}!function(e){e.Initial="Initial",e.Registered="Registered",e.Unregistered="Unregistered",e.Terminated="Terminated"}(ie=ie||(ie={}));class ge{constructor(e,t={}){this.disposed=!1,this._contacts=[],this._retryAfter=void 0,this._state=ie.Initial,this._waiting=!1,this._stateEventEmitter=new p,this._waitingEventEmitter=new p,this.userAgent=e;const s=e.configuration.uri.clone();if(s.user=void 0,this.options=Object.assign(Object.assign(Object.assign({},ge.defaultOptions()),{registrar:s}),ge.stripUndefinedProperties(t)),this.options.extraContactHeaderParams=(this.options.extraContactHeaderParams||[]).slice(),this.options.extraHeaders=(this.options.extraHeaders||[]).slice(),!this.options.registrar)throw new Error("Registrar undefined.");if(this.options.registrar=this.options.registrar.clone(),this.options.regId&&!this.options.instanceId?this.options.instanceId=this.userAgent.instanceId:!this.options.regId&&this.options.instanceId&&(this.options.regId=1),this.options.instanceId&&-1===y.parse(this.options.instanceId,"uuid"))throw new Error("Invalid instanceId.");if(this.options.regId&&this.options.regId<0)throw new Error("Invalid regId.");const i=this.options.registrar,r=this.options.params&&this.options.params.fromUri||e.userAgentCore.configuration.aor,n=this.options.params&&this.options.params.toUri||e.configuration.uri,o=this.options.params||{},a=(t.extraHeaders||[]).slice();if(this.request=e.userAgentCore.makeOutgoingRequestMessage(G.REGISTER,i,r,n,o,a,void 0),this.expires=this.options.expires||ge.defaultExpires,this.expires<0)throw new Error("Invalid expires.");if(this.refreshFrequency=this.options.refreshFrequency||ge.defaultRefreshFrequency,this.refreshFrequency<50||this.refreshFrequency>99)throw new Error("Invalid refresh frequency. The value represents a percentage of the expiration time and should be between 50 and 99.");this.logger=e.getLogger("sip.Registerer"),this.options.logConfiguration&&(this.logger.log("Configuration:"),Object.keys(this.options).forEach((e=>{const t=this.options[e];if("registrar"===e)this.logger.log("\xb7 "+e+": "+t);else this.logger.log("\xb7 "+e+": "+JSON.stringify(t))}))),this.id=this.request.callId+this.request.from.parameters.tag,this.userAgent._registerers[this.id]=this}static defaultOptions(){return{expires:ge.defaultExpires,extraContactHeaderParams:[],extraHeaders:[],logConfiguration:!0,instanceId:"",params:{},regId:0,registrar:new w("sip","anonymous","anonymous.invalid"),refreshFrequency:ge.defaultRefreshFrequency}}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}get contacts(){return this._contacts.slice()}get retryAfter(){return this._retryAfter}get state(){return this._state}get stateChange(){return this._stateEventEmitter}dispose(){return this.disposed?Promise.resolve():(this.disposed=!0,this.logger.log(`Registerer ${this.id} in state ${this.state} is being disposed`),delete this.userAgent._registerers[this.id],new Promise((e=>{const t=()=>{if(!this.waiting&&this._state===ie.Registered)return this.stateChange.addListener((()=>{this.terminated(),e()}),{once:!0}),void this.unregister();this.terminated(),e()};this.waiting?this.waitingChange.addListener((()=>{t()}),{once:!0}):t()})))}register(e={}){if(this.state===ie.Terminated)throw this.stateError(),new Error("Registerer terminated. Unable to register.");if(this.disposed)throw this.stateError(),new Error("Registerer disposed. Unable to register.");if(this.waiting){this.waitingWarning();const e=new a("REGISTER request already in progress, waiting for final response");return Promise.reject(e)}e.requestOptions&&(this.options=Object.assign(Object.assign({},this.options),e.requestOptions));const t=(this.options.extraHeaders||[]).slice();t.push("Contact: "+this.generateContactHeader(this.expires)),t.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),this.request.cseq++,this.request.setHeader("cseq",this.request.cseq+" REGISTER"),this.request.extraHeaders=t,this.waitingToggle(!0);const s=this.userAgent.userAgentCore.register(this.request,{onAccept:t=>{let s;t.message.hasHeader("expires")&&(s=Number(t.message.getHeader("expires"))),this._contacts=t.message.getHeaders("contact");let i,r=this._contacts.length;if(!r)return this.logger.error("No Contact header in response to REGISTER, dropping response."),void this.unregistered();for(;r--;){if(i=t.message.parseHeader("contact",r),!i)throw new Error("Contact undefined");if(this.userAgent.contact.pubGruu&&b(i.uri,this.userAgent.contact.pubGruu)){s=Number(i.getParam("expires"));break}if(""===this.userAgent.configuration.contactName){if(i.uri.user===this.userAgent.contact.uri.user){s=Number(i.getParam("expires"));break}}else if(b(i.uri,this.userAgent.contact.uri)){s=Number(i.getParam("expires"));break}i=void 0}if(void 0===i)return this.logger.error("No Contact header pointing to us, dropping response"),this.unregistered(),void this.waitingToggle(!1);if(void 0===s)return this.logger.error("Contact pointing to us is missing expires parameter, dropping response"),this.unregistered(),void this.waitingToggle(!1);if(i.hasParam("temp-gruu")){const e=i.getParam("temp-gruu");e&&(this.userAgent.contact.tempGruu=y.URIParse(e.replace(/"/g,"")))}if(i.hasParam("pub-gruu")){const e=i.getParam("pub-gruu");e&&(this.userAgent.contact.pubGruu=y.URIParse(e.replace(/"/g,"")))}this.registered(s),e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t),this.waitingToggle(!1)},onProgress:t=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)},onRedirect:t=>{this.logger.error("Redirect received. Not supported."),this.unregistered(),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t),this.waitingToggle(!1)},onReject:t=>{if(423===t.message.statusCode)return t.message.hasHeader("min-expires")?(this.expires=Number(t.message.getHeader("min-expires")),this.waitingToggle(!1),void this.register()):(this.logger.error("423 response received for REGISTER without Min-Expires, dropping response"),this.unregistered(),void this.waitingToggle(!1));this.logger.warn(`Failed to register, status code ${t.message.statusCode}`);let s=NaN;if(500===t.message.statusCode||503===t.message.statusCode){const e=t.message.getHeader("retry-after");e&&(s=Number.parseInt(e,void 0))}this._retryAfter=isNaN(s)?void 0:s,this.unregistered(),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t),this._retryAfter=void 0,this.waitingToggle(!1)},onTrying:t=>{e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}});return Promise.resolve(s)}unregister(e={}){if(this.state===ie.Terminated)throw this.stateError(),new Error("Registerer terminated. Unable to register.");if(this.disposed&&this.state!==ie.Registered)throw this.stateError(),new Error("Registerer disposed. Unable to register.");if(this.waiting){this.waitingWarning();const e=new a("REGISTER request already in progress, waiting for final response");return Promise.reject(e)}this._state===ie.Registered||e.all||this.logger.warn("Not currently registered, but sending an unregister anyway.");const t=(e.requestOptions&&e.requestOptions.extraHeaders||[]).slice();this.request.extraHeaders=t,e.all?(t.push("Contact: *"),t.push("Expires: 0")):t.push("Contact: "+this.generateContactHeader(0)),this.request.cseq++,this.request.setHeader("cseq",this.request.cseq+" REGISTER"),void 0!==this.registrationTimer&&(clearTimeout(this.registrationTimer),this.registrationTimer=void 0),this.waitingToggle(!0);const s=this.userAgent.userAgentCore.register(this.request,{onAccept:t=>{this._contacts=t.message.getHeaders("contact"),this.unregistered(),e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t),this.waitingToggle(!1)},onProgress:t=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)},onRedirect:t=>{this.logger.error("Unregister redirected. Not currently supported."),this.unregistered(),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t),this.waitingToggle(!1)},onReject:t=>{this.logger.error(`Unregister rejected with status code ${t.message.statusCode}`),this.unregistered(),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t),this.waitingToggle(!1)},onTrying:t=>{e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}});return Promise.resolve(s)}clearTimers(){void 0!==this.registrationTimer&&(clearTimeout(this.registrationTimer),this.registrationTimer=void 0),void 0!==this.registrationExpiredTimer&&(clearTimeout(this.registrationExpiredTimer),this.registrationExpiredTimer=void 0)}generateContactHeader(e){let t=this.userAgent.contact.toString({register:!0});return this.options.regId&&this.options.instanceId&&(t+=";reg-id="+this.options.regId,t+=';+sip.instance=""'),this.options.extraContactHeaderParams&&this.options.extraContactHeaderParams.forEach((e=>{t+=";"+e})),t+=";expires="+e,t}registered(e){this.clearTimers(),this.registrationTimer=setTimeout((()=>{this.registrationTimer=void 0,this.register()}),this.refreshFrequency/100*e*1e3),this.registrationExpiredTimer=setTimeout((()=>{this.logger.warn("Registration expired"),this.unregistered()}),1e3*e),this._state!==ie.Registered&&this.stateTransition(ie.Registered)}unregistered(){this.clearTimers(),this._state!==ie.Unregistered&&this.stateTransition(ie.Unregistered)}terminated(){this.clearTimers(),this._state!==ie.Terminated&&this.stateTransition(ie.Terminated)}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ie.Initial:e!==ie.Registered&&e!==ie.Unregistered&&e!==ie.Terminated&&t();break;case ie.Registered:e!==ie.Unregistered&&e!==ie.Terminated&&t();break;case ie.Unregistered:e!==ie.Registered&&e!==ie.Terminated&&t();break;case ie.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Registration transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===ie.Terminated&&this.dispose()}get waiting(){return this._waiting}get waitingChange(){return this._waitingEventEmitter}waitingToggle(e){if(this._waiting===e)throw new Error(`Invalid waiting transition from ${this._waiting} to ${e}`);this._waiting=e,this.logger.log(`Waiting toggled to ${this._waiting}`),this._waitingEventEmitter.emit(this._waiting)}waitingWarning(){let e="An attempt was made to send a REGISTER request while a prior one was still in progress.";e+=" RFC 3261 requires UAs MUST NOT send a new registration until they have received a final response",e+=" from the registrar for the previous one or the previous REGISTER request has timed out.",e+=" Note that if the transport disconnects, you still must wait for the prior request to time out before",e+=" sending a new REGISTER request or alternatively dispose of the current Registerer and create a new Registerer.",this.logger.warn("An attempt was made to send a REGISTER request while a prior one was still in progress. RFC 3261 requires UAs MUST NOT send a new registration until they have received a final response from the registrar for the previous one or the previous REGISTER request has timed out. Note that if the transport disconnects, you still must wait for the prior request to time out before sending a new REGISTER request or alternatively dispose of the current Registerer and create a new Registerer.")}stateError(){let e=`An attempt was made to send a REGISTER request when the Registerer ${this.state===ie.Terminated?"is in 'Terminated' state":"has been disposed"}.`;e+=" The Registerer transitions to 'Terminated' when Registerer.dispose() is called.",e+=" Perhaps you called UserAgent.stop() which dipsoses of all Registerers?",this.logger.error(e)}}ge.defaultExpires=600,ge.defaultRefreshFrequency=99,function(e){e.Initial="Initial",e.NotifyWait="NotifyWait",e.Pending="Pending",e.Active="Active",e.Terminated="Terminated"}(re=re||(re={})),function(e){e.Initial="Initial",e.NotifyWait="NotifyWait",e.Subscribed="Subscribed",e.Terminated="Terminated"}(ne=ne||(ne={}));class ue{constructor(e,t={}){this._disposed=!1,this._state=ne.Initial,this._logger=e.getLogger("sip.Subscription"),this._stateEventEmitter=new p,this._userAgent=e,this.delegate=t.delegate}dispose(){return this._disposed||(this._disposed=!0,this._stateEventEmitter.removeAllListeners()),Promise.resolve()}get dialog(){return this._dialog}get disposed(){return this._disposed}get state(){return this._state}get stateChange(){return this._stateEventEmitter}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ne.Initial:e!==ne.NotifyWait&&e!==ne.Terminated&&t();break;case ne.NotifyWait:e!==ne.Subscribed&&e!==ne.Terminated&&t();break;case ne.Subscribed:e!==ne.Terminated&&t();break;case ne.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state!==e&&(this._state=e,this._logger.log(`Subscription ${this._dialog?this._dialog.id:void 0} transitioned to ${this._state}`),this._stateEventEmitter.emit(this._state),e===ne.Terminated&&this.dispose())}}class pe extends ue{constructor(e,t,s,i={}){super(e,i),this.body=void 0,this.logger=e.getLogger("sip.Subscriber"),i.body&&(this.body={body:i.body,contentType:i.contentType?i.contentType:"application/sdp"}),this.targetURI=t,this.event=s,void 0===i.expires?this.expires=3600:"number"!=typeof i.expires?(this.logger.warn('Option "expires" must be a number. Using default of 3600.'),this.expires=3600):this.expires=i.expires,this.extraHeaders=(i.extraHeaders||[]).slice(),this.subscriberRequest=this.initSubscriberRequest(),this.outgoingRequestMessage=this.subscriberRequest.message,this.id=this.outgoingRequestMessage.callId+this.outgoingRequestMessage.from.parameters.tag+this.event,this._userAgent._subscriptions[this.id]=this}dispose(){return this.disposed?Promise.resolve():(this.logger.log(`Subscription ${this.id} in state ${this.state} is being disposed`),delete this._userAgent._subscriptions[this.id],this.retryAfterTimer&&(clearTimeout(this.retryAfterTimer),this.retryAfterTimer=void 0),this.subscriberRequest.dispose(),super.dispose().then((()=>{if(this.state===ne.Subscribed){if(!this._dialog)throw new Error("Dialog undefined.");if(this._dialog.subscriptionState===re.Pending||this._dialog.subscriptionState===re.Active){const e=this._dialog;return new Promise(((t,s)=>{e.delegate={onTerminated:()=>t()},e.unsubscribe()}))}}})))}subscribe(e={}){switch(this.subscriberRequest.state){case re.Initial:this.state===ne.Initial&&this.stateTransition(ne.NotifyWait),this.subscriberRequest.subscribe().then((e=>{e.success?(e.success.subscription&&(this._dialog=e.success.subscription,this._dialog.delegate={onNotify:e=>this.onNotify(e),onRefresh:e=>this.onRefresh(e),onTerminated:()=>{this.state!==ne.Terminated&&this.stateTransition(ne.Terminated)}}),this.onNotify(e.success.request)):e.failure&&this.unsubscribe()}));break;case re.NotifyWait:case re.Pending:break;case re.Active:if(this._dialog){this._dialog.refresh().delegate={onAccept:e=>this.onAccepted(e),onRedirect:e=>this.unsubscribe(),onReject:e=>this.unsubscribe()}}case re.Terminated:}return Promise.resolve()}unsubscribe(e={}){if(this.disposed)return Promise.resolve();switch(this.subscriberRequest.state){case re.Initial:case re.NotifyWait:break;case re.Pending:case re.Active:this._dialog&&this._dialog.unsubscribe();break;case re.Terminated:break;default:throw new Error("Unknown state.")}return this.stateTransition(ne.Terminated),Promise.resolve()}_refresh(){return this.subscriberRequest.state===re.Active?this.subscribe():Promise.resolve()}onAccepted(e){}onNotify(e){if(this.disposed)return void e.accept();if(this.state!==ne.Subscribed&&this.stateTransition(ne.Subscribed),this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else e.accept();const t=e.message.parseHeader("Subscription-State");if(t&&t.state&&"terminated"===t.state){if(t.reason)switch(this.logger.log(`Terminated subscription with reason ${t.reason}`),t.reason){case"deactivated":case"timeout":return this.initSubscriberRequest(),void this.subscribe();case"probation":case"giveup":return this.initSubscriberRequest(),void(t.params&&t.params["retry-after"]?this.retryAfterTimer=setTimeout((()=>{this.subscribe()}),t.params["retry-after"]):this.subscribe())}this.unsubscribe()}}onRefresh(e){e.delegate={onAccept:e=>this.onAccepted(e)}}initSubscriberRequest(){const e={extraHeaders:this.extraHeaders,body:this.body?q(this.body):void 0};return this.subscriberRequest=new fe(this._userAgent.userAgentCore,this.targetURI,this.event,this.expires,e),this.subscriberRequest.delegate={onAccept:e=>this.onAccepted(e)},this.subscriberRequest}}class fe{constructor(e,t,s,i,r,n){this.core=e,this.target=t,this.event=s,this.expires=i,this.subscribed=!1,this.logger=e.loggerFactory.getLogger("sip.Subscriber"),this.delegate=n;const o="Allow: "+V.toString(),a=(r&&r.extraHeaders||[]).slice();a.push(o),a.push("Event: "+this.event),a.push("Expires: "+this.expires),a.push("Contact: "+this.core.configuration.contact.toString());const c=r&&r.body;this.message=e.makeOutgoingRequestMessage(G.SUBSCRIBE,this.target,this.core.configuration.aor,this.target,{},a,c)}dispose(){this.request&&(this.request.waitNotifyStop(),this.request.dispose(),this.request=void 0)}get state(){return this.subscription?this.subscription.subscriptionState:this.subscribed?re.NotifyWait:re.Initial}subscribe(){return this.subscribed?Promise.reject(new Error("Not in initial state. Did you call subscribe more than once?")):(this.subscribed=!0,new Promise((e=>{if(!this.message)throw new Error("Message undefined.");this.request=this.core.subscribe(this.message,{onAccept:e=>{this.delegate&&this.delegate.onAccept&&this.delegate.onAccept(e)},onNotify:t=>{this.subscription=t.subscription,this.subscription&&(this.subscription.autoRefresh=!0),e({success:t})},onNotifyTimeout:()=>{e({failure:{}})},onRedirect:t=>{e({failure:{response:t}})},onReject:t=>{e({failure:{response:t}})}})})))}}!function(e){e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Disconnected="Disconnected"}(oe=oe||(oe={})),function(e){e.Started="Started",e.Stopped="Stopped"}(ae=ae||(ae={}));class me{constructor(){this._dataLength=0,this._bufferLength=0,this._state=new Int32Array(4),this._buffer=new ArrayBuffer(68),this._buffer8=new Uint8Array(this._buffer,0,68),this._buffer32=new Uint32Array(this._buffer,0,17),this.start()}static hashStr(e,t=!1){return this.onePassHasher.start().appendStr(e).end(t)}static hashAsciiStr(e,t=!1){return this.onePassHasher.start().appendAsciiStr(e).end(t)}static _hex(e){const t=me.hexChars,s=me.hexOut;let i,r,n,o;for(o=0;o<4;o+=1)for(r=8*o,i=e[o],n=0;n<8;n+=2)s[r+1+n]=t.charAt(15&i),i>>>=4,s[r+0+n]=t.charAt(15&i),i>>>=4;return s.join("")}static _md5cycle(e,t){let s=e[0],i=e[1],r=e[2],n=e[3];s+=(i&r|~i&n)+t[0]-680876936|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[1]-389564586|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[2]+606105819|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[3]-1044525330|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[4]-176418897|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[5]+1200080426|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[6]-1473231341|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[7]-45705983|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[8]+1770035416|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[9]-1958414417|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[10]-42063|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[11]-1990404162|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[12]+1804603682|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[13]-40341101|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[14]-1502002290|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[15]+1236535329|0,i=(i<<22|i>>>10)+r|0,s+=(i&n|r&~n)+t[1]-165796510|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[6]-1069501632|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[11]+643717713|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[0]-373897302|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[5]-701558691|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[10]+38016083|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[15]-660478335|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[4]-405537848|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[9]+568446438|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[14]-1019803690|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[3]-187363961|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[8]+1163531501|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[13]-1444681467|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[2]-51403784|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[7]+1735328473|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[12]-1926607734|0,i=(i<<20|i>>>12)+r|0,s+=(i^r^n)+t[5]-378558|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[8]-2022574463|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[11]+1839030562|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[14]-35309556|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[1]-1530992060|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[4]+1272893353|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[7]-155497632|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[10]-1094730640|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[13]+681279174|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[0]-358537222|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[3]-722521979|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[6]+76029189|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[9]-640364487|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[12]-421815835|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[15]+530742520|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[2]-995338651|0,i=(i<<23|i>>>9)+r|0,s+=(r^(i|~n))+t[0]-198630844|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[7]+1126891415|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[14]-1416354905|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[5]-57434055|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[12]+1700485571|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[3]-1894986606|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[10]-1051523|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[1]-2054922799|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[8]+1873313359|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[15]-30611744|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[6]-1560198380|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[13]+1309151649|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[4]-145523070|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[11]-1120210379|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[2]+718787259|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[9]-343485551|0,i=(i<<21|i>>>11)+r|0,e[0]=s+e[0]|0,e[1]=i+e[1]|0,e[2]=r+e[2]|0,e[3]=n+e[3]|0}start(){return this._dataLength=0,this._bufferLength=0,this._state.set(me.stateIdentity),this}appendStr(e){const t=this._buffer8,s=this._buffer32;let i,r,n=this._bufferLength;for(r=0;r>>6),t[n++]=63&i|128;else if(i<55296||i>56319)t[n++]=224+(i>>>12),t[n++]=i>>>6&63|128,t[n++]=63&i|128;else{if(i=1024*(i-55296)+(e.charCodeAt(++r)-56320)+65536,i>1114111)throw new Error("Unicode standard supports code points up to U+10FFFF");t[n++]=240+(i>>>18),t[n++]=i>>>12&63|128,t[n++]=i>>>6&63|128,t[n++]=63&i|128}n>=64&&(this._dataLength+=64,me._md5cycle(this._state,s),n-=64,s[0]=s[16])}return this._bufferLength=n,this}appendAsciiStr(e){const t=this._buffer8,s=this._buffer32;let i,r=this._bufferLength,n=0;for(;;){for(i=Math.min(e.length-n,64-r);i--;)t[r++]=e.charCodeAt(n++);if(r<64)break;this._dataLength+=64,me._md5cycle(this._state,s),r=0}return this._bufferLength=r,this}appendByteArray(e){const t=this._buffer8,s=this._buffer32;let i,r=this._bufferLength,n=0;for(;;){for(i=Math.min(e.length-n,64-r);i--;)t[r++]=e[n++];if(r<64)break;this._dataLength+=64,me._md5cycle(this._state,s),r=0}return this._bufferLength=r,this}getState(){const e=this,t=e._state;return{buffer:String.fromCharCode.apply(null,e._buffer8),buflen:e._bufferLength,length:e._dataLength,state:[t[0],t[1],t[2],t[3]]}}setState(e){const t=e.buffer,s=e.state,i=this._state;let r;for(this._dataLength=e.length,this._bufferLength=e.buflen,i[0]=s[0],i[1]=s[1],i[2]=s[2],i[3]=s[3],r=0;r>2);let n;if(this._dataLength+=t,s[t]=128,s[t+1]=s[t+2]=s[t+3]=0,i.set(me.buffer32Identity.subarray(r),r),t>55&&(me._md5cycle(this._state,i),i.set(me.buffer32Identity)),n=8*this._dataLength,n<=4294967295)i[14]=n;else{const e=n.toString(16).match(/(.*?)(.{0,8})$/);if(null===e)return;const t=parseInt(e[2],16),s=parseInt(e[1],16)||0;i[14]=t,i[15]=s}return me._md5cycle(this._state,i),e?this._state:me._hex(this._state)}}function ve(e){return me.hashStr(e)}me.stateIdentity=new Int32Array([1732584193,-271733879,-1732584194,271733878]),me.buffer32Identity=new Int32Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]),me.hexChars="0123456789abcdef",me.hexOut=[],me.onePassHasher=new me,"5d41402abc4b2a76b9719d911017c592"!==me.hashStr("hello")&&console.error("Md5 self test failed.");class we{constructor(e,t,s,i){this.logger=e.getLogger("sipjs.digestauthentication"),this.username=s,this.password=i,this.ha1=t,this.nc=0,this.ncHex="00000000"}authenticate(e,t,s){if(this.algorithm=t.algorithm,this.realm=t.realm,this.nonce=t.nonce,this.opaque=t.opaque,this.stale=t.stale,this.algorithm){if("MD5"!==this.algorithm)return this.logger.warn("challenge with Digest algorithm different than 'MD5', authentication aborted"),!1}else this.algorithm="MD5";if(!this.realm)return this.logger.warn("challenge without Digest realm, authentication aborted"),!1;if(!this.nonce)return this.logger.warn("challenge without Digest nonce, authentication aborted"),!1;if(t.qop)if(t.qop.indexOf("auth")>-1)this.qop="auth";else{if(!(t.qop.indexOf("auth-int")>-1))return this.logger.warn("challenge without Digest qop different than 'auth' or 'auth-int', authentication aborted"),!1;this.qop="auth-int"}else this.qop=void 0;return this.method=e.method,this.uri=e.ruri,this.cnonce=$(12),this.nc+=1,this.updateNcHex(),4294967296===this.nc&&(this.nc=1,this.ncHex="00000001"),this.calculateResponse(s),!0}toString(){const e=[];if(!this.response)throw new Error("response field does not exist, cannot generate Authorization header");return e.push("algorithm="+this.algorithm),e.push('username="'+this.username+'"'),e.push('realm="'+this.realm+'"'),e.push('nonce="'+this.nonce+'"'),e.push('uri="'+this.uri+'"'),e.push('response="'+this.response+'"'),this.opaque&&e.push('opaque="'+this.opaque+'"'),this.qop&&(e.push("qop="+this.qop),e.push('cnonce="'+this.cnonce+'"'),e.push("nc="+this.ncHex)),"Digest "+e.join(", ")}updateNcHex(){const e=Number(this.nc).toString(16);this.ncHex="00000000".substr(0,8-e.length)+e}calculateResponse(e){let t,s;t=this.ha1,""!==t&&void 0!==t||(t=ve(this.username+":"+this.realm+":"+this.password)),"auth"===this.qop?(s=ve(this.method+":"+this.uri),this.response=ve(t+":"+this.nonce+":"+this.ncHex+":"+this.cnonce+":auth:"+s)):"auth-int"===this.qop?(s=ve(this.method+":"+this.uri+":"+ve(e||"")),this.response=ve(t+":"+this.nonce+":"+this.ncHex+":"+this.cnonce+":auth-int:"+s)):void 0===this.qop&&(s=ve(this.method+":"+this.uri),this.response=ve(t+":"+this.nonce+":"+s))}}!function(e){e[e.error=0]="error",e[e.warn=1]="warn",e[e.log=2]="log",e[e.debug=3]="debug"}(ce=ce||(ce={}));class be{constructor(e,t,s){this.logger=e,this.category=t,this.label=s}error(e){this.genericLog(ce.error,e)}warn(e){this.genericLog(ce.warn,e)}log(e){this.genericLog(ce.log,e)}debug(e){this.genericLog(ce.debug,e)}genericLog(e,t){this.logger.genericLog(e,this.category,this.label,t)}get level(){return this.logger.level}set level(e){this.logger.level=e}}class Te{constructor(){this.builtinEnabled=!0,this._level=ce.log,this.loggers={},this.logger=this.getLogger("sip:loggerfactory")}get level(){return this._level}set level(e){e>=0&&e<=3?this._level=e:e>3?this._level=3:ce.hasOwnProperty(e)?this._level=e:this.logger.error("invalid 'level' parameter value: "+JSON.stringify(e))}get connector(){return this._connector}set connector(e){e?"function"==typeof e?this._connector=e:this.logger.error("invalid 'connector' parameter value: "+JSON.stringify(e)):this._connector=void 0}getLogger(e,t){if(t&&3===this.level)return new be(this,e,t);if(this.loggers[e])return this.loggers[e];{const t=new be(this,e);return this.loggers[e]=t,t}}genericLog(e,t,s,i){this.level>=e&&this.builtinEnabled&&this.print(e,t,s,i),this.connector&&this.connector(ce[e],t,s,i)}print(e,t,s,i){if("string"==typeof i){const e=[new Date,t];s&&e.push(s),i=e.concat(i).join(" | ")}switch(e){case ce.error:console.error(i);break;case ce.warn:console.warn(i);break;case ce.log:console.log(i);break;case ce.debug:console.debug(i)}}}function Se(e,t){const s="\r\n";if(t.statusCode<100||t.statusCode>699)throw new TypeError("Invalid statusCode: "+t.statusCode);const i=t.reasonPhrase?t.reasonPhrase:I(t.statusCode);let r="SIP/2.0 "+t.statusCode+" "+i+s;t.statusCode>=100&&t.statusCode,t.statusCode;const n="From: "+e.getHeader("From")+s,o="Call-ID: "+e.callId+s,a="CSeq: "+e.cseq+" "+e.method+s,c=e.getHeaders("via").reduce(((e,t)=>e+"Via: "+t+s),"");let h="To: "+e.getHeader("to");if(t.statusCode>100&&!e.parseHeader("to").hasParam("tag")){let e=t.toTag;e||(e=C()),h+=";tag="+e}h+=s;let d="";t.supported&&(d="Supported: "+t.supported.join(", ")+s);let l="";t.userAgent&&(l="User-Agent: "+t.userAgent+s);let g="";return t.extraHeaders&&(g=t.extraHeaders.reduce(((e,t)=>e+t.trim()+s),"")),r+=c,r+=n,r+=h,r+=a,r+=o,r+=d,r+=l,r+=g,t.body?(r+="Content-Type: "+t.body.contentType+s,r+="Content-Length: "+D(t.body.content)+s+s,r+=t.body.content):r+="Content-Length: 0\r\n\r\n",{message:r}}!function(e){function t(e,t){let s=t,i=0,r=0;if(e.substring(s,s+2).match(/(^\r\n)/))return-2;for(;0===i;){if(r=e.indexOf("\r\n",s),-1===r)return r;!e.substring(r+2,r+4).match(/(^\r\n)/)&&e.charAt(r+2).match(/(^\s+)/)?s=r+2:i=r}return i}function s(e,t,s,i){const r=t.indexOf(":",s),n=t.substring(s,r).trim(),o=t.substring(r+1,i).trim();let a;switch(n.toLowerCase()){case"via":case"v":e.addHeader("via",o),1===e.getHeaders("via").length?(a=e.parseHeader("Via"),a&&(e.via=a,e.viaBranch=a.branch)):a=0;break;case"from":case"f":e.setHeader("from",o),a=e.parseHeader("from"),a&&(e.from=a,e.fromTag=a.getParam("tag"));break;case"to":case"t":e.setHeader("to",o),a=e.parseHeader("to"),a&&(e.to=a,e.toTag=a.getParam("tag"));break;case"record-route":if(a=y.parse(o,"Record_Route"),-1===a){a=void 0;break}if(!(a instanceof Array)){a=void 0;break}a.forEach((t=>{e.addHeader("record-route",o.substring(t.position,t.offset)),e.headers["Record-Route"][e.getHeaders("record-route").length-1].parsed=t.parsed}));break;case"call-id":case"i":e.setHeader("call-id",o),a=e.parseHeader("call-id"),a&&(e.callId=o);break;case"contact":case"m":if(a=y.parse(o,"Contact"),-1===a){a=void 0;break}if(!(a instanceof Array)){a=void 0;break}a.forEach((t=>{e.addHeader("contact",o.substring(t.position,t.offset)),e.headers.Contact[e.getHeaders("contact").length-1].parsed=t.parsed}));break;case"content-length":case"l":e.setHeader("content-length",o),a=e.parseHeader("content-length");break;case"content-type":case"c":e.setHeader("content-type",o),a=e.parseHeader("content-type");break;case"cseq":e.setHeader("cseq",o),a=e.parseHeader("cseq"),a&&(e.cseq=a.value),e instanceof x&&(e.method=a.method);break;case"max-forwards":e.setHeader("max-forwards",o),a=e.parseHeader("max-forwards");break;case"www-authenticate":e.setHeader("www-authenticate",o),a=e.parseHeader("www-authenticate");break;case"proxy-authenticate":e.setHeader("proxy-authenticate",o),a=e.parseHeader("proxy-authenticate");break;case"refer-to":case"r":e.setHeader("refer-to",o),a=e.parseHeader("refer-to"),a&&(e.referTo=a);break;default:e.addHeader(n.toLowerCase(),o),a=0}return void 0!==a||{error:"error parsing header '"+n+"'"}}e.getHeader=t,e.parseHeader=s,e.parseMessage=function(e,i){let r=0,n=e.indexOf("\r\n");if(-1===n)return void i.warn("no CRLF found, not a SIP message, discarded");const o=e.substring(0,n),a=y.parse(o,"Request_Response");let c,h;if(-1!==a){for(a.status_code?(c=new x,c.statusCode=a.status_code,c.reasonPhrase=a.reason_phrase):(c=new P,c.method=a.method,c.ruri=a.uri),c.data=e,r=n+2;;){if(n=t(e,r),-2===n){h=r+2;break}if(-1===n)return void i.error("malformed message");const o=s(c,e,r,n);if(o&&!0!==o)return void i.error(o.error);r=n+2}return c.hasHeader("content-length")?c.body=e.substr(h,Number(c.getHeader("content-length"))):c.body=e.substring(h),c}i.warn('error parsing first line of SIP message: "'+o+'"')}}(he=he||(he={}));class Re extends n{constructor(e){super(e||"Unspecified transport error.")}}class ye{constructor(e,t,s,i,r){this._transport=e,this._user=t,this._id=s,this._state=i,this.listeners=new Array,this.logger=t.loggerFactory.getLogger(r,s),this.logger.debug(`Constructing ${this.typeToString()} with id ${this.id}.`)}dispose(){this.logger.debug(`Destroyed ${this.typeToString()} with id ${this.id}.`)}get id(){return this._id}get kind(){throw new Error("Invalid kind.")}get state(){return this._state}get transport(){return this._transport}addStateChangeListener(e,t){const s=()=>{this.removeStateChangeListener(s),e()};!0===(null==t?void 0:t.once)?this.listeners.push(s):this.listeners.push(e)}notifyStateChangeListeners(){this.listeners.slice().forEach((e=>e()))}removeStateChangeListener(e){this.listeners=this.listeners.filter((t=>t!==e))}logTransportError(e,t){this.logger.error(e.message),this.logger.error(`Transport error occurred in ${this.typeToString()} with id ${this.id}.`),this.logger.error(t)}send(e){return this.transport.send(e).catch((e=>{if(e instanceof Re)throw this.onTransportError(e),e;let t;throw t=e&&"string"==typeof e.message?new Re(e.message):new Re,this.onTransportError(t),t}))}setState(e){this.logger.debug(`State change to "${e}" on ${this.typeToString()} with id ${this.id}.`),this._state=e,this._user.onStateChange&&this._user.onStateChange(e),this.notifyStateChangeListeners()}typeToString(){return"UnknownType"}}class Ee extends ye{constructor(e,t,s,i,r){super(t,s,e.viaBranch,i,r),this._request=e,this.user=s}get request(){return this._request}}!function(e){e.Accepted="Accepted",e.Calling="Calling",e.Completed="Completed",e.Confirmed="Confirmed",e.Proceeding="Proceeding",e.Terminated="Terminated",e.Trying="Trying"}(de=de||(de={}));class $e extends Ee{constructor(e,t,s){super(e,t,s,de.Proceeding,"sip.transaction.ist")}dispose(){this.stopProgressExtensionTimer(),this.H&&(clearTimeout(this.H),this.H=void 0),this.I&&(clearTimeout(this.I),this.I=void 0),this.L&&(clearTimeout(this.L),this.L=void 0),super.dispose()}get kind(){return"ist"}receiveRequest(e){switch(this.state){case de.Proceeding:if(e.method===G.INVITE)return void(this.lastProvisionalResponse&&this.send(this.lastProvisionalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")})));break;case de.Accepted:if(e.method===G.INVITE)return;break;case de.Completed:if(e.method===G.INVITE){if(!this.lastFinalResponse)throw new Error("Last final response undefined.");return void this.send(this.lastFinalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of final response.")}))}if(e.method===G.ACK)return void this.stateTransition(de.Confirmed);break;case de.Confirmed:case de.Terminated:if(e.method===G.INVITE||e.method===G.ACK)return;break;default:throw new Error(`Invalid state ${this.state}`)}const t=`INVITE server transaction received unexpected ${e.method} request while in state ${this.state}.`;this.logger.warn(t)}receiveResponse(e,t){if(e<100||e>699)throw new Error(`Invalid status code ${e}`);switch(this.state){case de.Proceeding:if(e>=100&&e<=199)return this.lastProvisionalResponse=t,e>100&&this.startProgressExtensionTimer(),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 1xx response.")}));if(e>=200&&e<=299)return this.lastFinalResponse=t,this.stateTransition(de.Accepted),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}));if(e>=300&&e<=699)return this.lastFinalResponse=t,this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send non-2xx final response.")}));break;case de.Accepted:if(e>=200&&e<=299)return void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}));break;case de.Completed:case de.Confirmed:case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`INVITE server transaction received unexpected ${e} response from TU while in state ${this.state}.`;throw this.logger.error(s),new Error(s)}retransmitAcceptedResponse(){this.state===de.Accepted&&this.lastFinalResponse&&this.send(this.lastFinalResponse).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}))}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e)}typeToString(){return"INVITE server transaction"}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Proceeding:t();break;case de.Accepted:case de.Completed:this.state!==de.Proceeding&&t();break;case de.Confirmed:this.state!==de.Completed&&t();break;case de.Terminated:this.state!==de.Accepted&&this.state!==de.Completed&&this.state!==de.Confirmed&&t();break;default:t()}this.stopProgressExtensionTimer(),e===de.Accepted&&(this.L=setTimeout((()=>this.timerL()),L.TIMER_L)),e===de.Completed&&(this.H=setTimeout((()=>this.timerH()),L.TIMER_H)),e===de.Confirmed&&(this.I=setTimeout((()=>this.timerI()),L.TIMER_I)),e===de.Terminated&&this.dispose(),this.setState(e)}startProgressExtensionTimer(){void 0===this.progressExtensionTimer&&(this.progressExtensionTimer=setInterval((()=>{if(this.logger.debug(`Progress extension timer expired for INVITE server transaction ${this.id}.`),!this.lastProvisionalResponse)throw new Error("Last provisional response undefined.");this.send(this.lastProvisionalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")}))}),L.PROVISIONAL_RESPONSE_INTERVAL))}stopProgressExtensionTimer(){void 0!==this.progressExtensionTimer&&(clearInterval(this.progressExtensionTimer),this.progressExtensionTimer=void 0)}timerG(){}timerH(){this.logger.debug(`Timer H expired for INVITE server transaction ${this.id}.`),this.state===de.Completed&&(this.logger.warn("ACK to negative final response was never received, terminating transaction."),this.stateTransition(de.Terminated))}timerI(){this.logger.debug(`Timer I expired for INVITE server transaction ${this.id}.`),this.stateTransition(de.Terminated)}timerL(){this.logger.debug(`Timer L expired for INVITE server transaction ${this.id}.`),this.state===de.Accepted&&this.stateTransition(de.Terminated)}}class Ie extends ye{constructor(e,t,s,i,r){super(t,s,Ie.makeId(e),i,r),this._request=e,this.user=s,e.setViaHeader(this.id,t.protocol)}static makeId(e){if("CANCEL"===e.method){if(!e.branch)throw new Error("Outgoing CANCEL request without a branch.");return e.branch}return"z9hG4bK"+Math.floor(1e7*Math.random())}get request(){return this._request}onRequestTimeout(){this.user.onRequestTimeout&&this.user.onRequestTimeout()}}class Ce extends Ie{constructor(e,t,s){super(e,t,s,de.Trying,"sip.transaction.nict"),this.F=setTimeout((()=>this.timerF()),L.TIMER_F),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send initial outgoing request.")}))}dispose(){this.F&&(clearTimeout(this.F),this.F=void 0),this.K&&(clearTimeout(this.K),this.K=void 0),super.dispose()}get kind(){return"nict"}receiveResponse(e){const t=e.statusCode;if(!t||t<100||t>699)throw new Error(`Invalid status code ${t}`);switch(this.state){case de.Trying:if(t>=100&&t<=199)return this.stateTransition(de.Proceeding),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=699)return this.stateTransition(de.Completed),408===t?void this.onRequestTimeout():void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Proceeding:if(t>=100&&t<=199&&this.user.receiveResponse)return this.user.receiveResponse(e);if(t>=200&&t<=699)return this.stateTransition(de.Completed),408===t?void this.onRequestTimeout():void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Completed:case de.Terminated:return;default:throw new Error(`Invalid state ${this.state}`)}const s=`Non-INVITE client transaction received unexpected ${t} response while in state ${this.state}.`;this.logger.warn(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"non-INVITE client transaction"}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Trying:s();break;case de.Proceeding:this.state!==de.Trying&&s();break;case de.Completed:this.state!==de.Trying&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Trying&&this.state!==de.Proceeding&&this.state!==de.Completed&&(t||s());break;default:s()}e===de.Completed&&(this.F&&(clearTimeout(this.F),this.F=void 0),this.K=setTimeout((()=>this.timerK()),L.TIMER_K)),e===de.Terminated&&this.dispose(),this.setState(e)}timerF(){this.logger.debug(`Timer F expired for non-INVITE client transaction ${this.id}.`),this.state!==de.Trying&&this.state!==de.Proceeding||(this.onRequestTimeout(),this.stateTransition(de.Terminated))}timerK(){this.state===de.Completed&&this.stateTransition(de.Terminated)}}class Ae{constructor(e,t){this.core=e,this.dialogState=t,this.core.dialogs.set(this.id,this)}static initialDialogStateForUserAgentClient(e,t){const s=t.getHeaders("record-route").reverse(),i=t.parseHeader("contact");if(!i)throw new Error("Contact undefined.");if(!(i instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const r=i.uri,n=e.cseq,o=e.callId,a=e.fromTag,c=t.toTag;if(!o)throw new Error("Call id undefined.");if(!a)throw new Error("From tag undefined.");if(!c)throw new Error("To tag undefined.");if(!e.from)throw new Error("From undefined.");if(!e.to)throw new Error("To undefined.");const h=e.from.uri,d=e.to.uri;if(!t.statusCode)throw new Error("Incoming response status code undefined.");return{id:o+a+c,early:t.statusCode<200,callId:o,localTag:a,remoteTag:c,localSequenceNumber:n,remoteSequenceNumber:undefined,localURI:h,remoteURI:d,remoteTarget:r,routeSet:s,secure:!1}}static initialDialogStateForUserAgentServer(e,t,s=!1){const i=e.getHeaders("record-route"),r=e.parseHeader("contact");if(!r)throw new Error("Contact undefined.");if(!(r instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const n=r.uri,o=e.cseq,a=e.callId,c=t,h=e.fromTag,d=e.from.uri;return{id:a+c+h,early:s,callId:a,localTag:c,remoteTag:h,localSequenceNumber:undefined,remoteSequenceNumber:o,localURI:e.to.uri,remoteURI:d,remoteTarget:n,routeSet:i,secure:!1}}dispose(){this.core.dialogs.delete(this.id)}get id(){return this.dialogState.id}get early(){return this.dialogState.early}get callId(){return this.dialogState.callId}get localTag(){return this.dialogState.localTag}get remoteTag(){return this.dialogState.remoteTag}get localSequenceNumber(){return this.dialogState.localSequenceNumber}get remoteSequenceNumber(){return this.dialogState.remoteSequenceNumber}get localURI(){return this.dialogState.localURI}get remoteURI(){return this.dialogState.remoteURI}get remoteTarget(){return this.dialogState.remoteTarget}get routeSet(){return this.dialogState.routeSet}get secure(){return this.dialogState.secure}get userAgentCore(){return this.core}confirm(){this.dialogState.early=!1}receiveRequest(e){if(e.method!==G.ACK){if(this.remoteSequenceNumber){if(e.cseq<=this.remoteSequenceNumber)throw new Error("Out of sequence in dialog request. Did you forget to call sequenceGuard()?");this.dialogState.remoteSequenceNumber=e.cseq}this.remoteSequenceNumber||(this.dialogState.remoteSequenceNumber=e.cseq)}}recomputeRouteSet(e){this.dialogState.routeSet=e.getHeaders("record-route").reverse()}createOutgoingRequestMessage(e,t){const s=this.remoteURI,i=this.remoteTag,r=this.localURI,n=this.localTag,o=this.callId;let a;a=t&&t.cseq?t.cseq:this.dialogState.localSequenceNumber?this.dialogState.localSequenceNumber+=1:this.dialogState.localSequenceNumber=1;const c=this.remoteTarget,h=this.routeSet,d=t&&t.extraHeaders,l=t&&t.body;return this.userAgentCore.makeOutgoingRequestMessage(e,c,r,s,{callId:o,cseq:a,fromTag:n,toTag:i,routeSet:h},d,l)}incrementLocalSequenceNumber(){if(!this.dialogState.localSequenceNumber)throw new Error("Local sequence number undefined.");this.dialogState.localSequenceNumber+=1}sequenceGuard(e){return e.method===G.ACK||(!(this.remoteSequenceNumber&&e.cseq<=this.remoteSequenceNumber)||(this.core.replyStateless(e,{statusCode:500}),!1))}}class De extends Ie{constructor(e,t,s){super(e,t,s,de.Calling,"sip.transaction.ict"),this.ackRetransmissionCache=new Map,this.B=setTimeout((()=>this.timerB()),L.TIMER_B),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send initial outgoing request.")}))}dispose(){this.B&&(clearTimeout(this.B),this.B=void 0),this.D&&(clearTimeout(this.D),this.D=void 0),this.M&&(clearTimeout(this.M),this.M=void 0),super.dispose()}get kind(){return"ict"}ackResponse(e){const t=e.toTag;if(!t)throw new Error("To tag undefined.");const s="z9hG4bK"+Math.floor(1e7*Math.random());e.setViaHeader(s,this.transport.protocol),this.ackRetransmissionCache.set(t,e),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send ACK to 2xx response.")}))}receiveResponse(e){const t=e.statusCode;if(!t||t<100||t>699)throw new Error(`Invalid status code ${t}`);switch(this.state){case de.Calling:if(t>=100&&t<=199)return this.stateTransition(de.Proceeding),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=299)return this.ackRetransmissionCache.set(e.toTag,void 0),this.stateTransition(de.Accepted),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=300&&t<=699)return this.stateTransition(de.Completed),this.ack(e),void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Proceeding:if(t>=100&&t<=199)return void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=299)return this.ackRetransmissionCache.set(e.toTag,void 0),this.stateTransition(de.Accepted),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=300&&t<=699)return this.stateTransition(de.Completed),this.ack(e),void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Accepted:if(t>=200&&t<=299){if(!this.ackRetransmissionCache.has(e.toTag))return this.ackRetransmissionCache.set(e.toTag,void 0),void(this.user.receiveResponse&&this.user.receiveResponse(e));const t=this.ackRetransmissionCache.get(e.toTag);return t?void this.send(t.toString()).catch((e=>{this.logTransportError(e,"Failed to send retransmission of ACK to 2xx response.")})):void 0}break;case de.Completed:if(t>=300&&t<=699)return void this.ack(e);break;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`Received unexpected ${t} response while in state ${this.state}.`;this.logger.warn(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"INVITE client transaction"}ack(e){const t=this.request.ruri,s=this.request.callId,i=this.request.cseq,r=this.request.getHeader("from"),n=e.getHeader("to"),o=this.request.getHeader("via"),a=this.request.getHeader("route");if(!r)throw new Error("From undefined.");if(!n)throw new Error("To undefined.");if(!o)throw new Error("Via undefined.");let c=`ACK ${t} SIP/2.0\r\n`;a&&(c+=`Route: ${a}\r\n`),c+=`Via: ${o}\r\n`,c+=`To: ${n}\r\n`,c+=`From: ${r}\r\n`,c+=`Call-ID: ${s}\r\n`,c+=`CSeq: ${i} ACK\r\n`,c+="Max-Forwards: 70\r\n",c+="Content-Length: 0\r\n\r\n",this.send(c).catch((e=>{this.logTransportError(e,"Failed to send ACK to non-2xx response.")}))}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Calling:s();break;case de.Proceeding:this.state!==de.Calling&&s();break;case de.Accepted:case de.Completed:this.state!==de.Calling&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Calling&&this.state!==de.Accepted&&this.state!==de.Completed&&(t||s());break;default:s()}this.B&&(clearTimeout(this.B),this.B=void 0),de.Proceeding,e===de.Completed&&(this.D=setTimeout((()=>this.timerD()),L.TIMER_D)),e===de.Accepted&&(this.M=setTimeout((()=>this.timerM()),L.TIMER_M)),e===de.Terminated&&this.dispose(),this.setState(e)}timerA(){}timerB(){this.logger.debug(`Timer B expired for INVITE client transaction ${this.id}.`),this.state===de.Calling&&(this.onRequestTimeout(),this.stateTransition(de.Terminated))}timerD(){this.logger.debug(`Timer D expired for INVITE client transaction ${this.id}.`),this.state===de.Completed&&this.stateTransition(de.Terminated)}timerM(){this.logger.debug(`Timer M expired for INVITE client transaction ${this.id}.`),this.state===de.Accepted&&this.stateTransition(de.Terminated)}}class He{constructor(e,t,s,i){this.transactionConstructor=e,this.core=t,this.message=s,this.delegate=i,this.challenged=!1,this.stale=!1,this.logger=this.loggerFactory.getLogger("sip.user-agent-client"),this.init()}dispose(){this.transaction.dispose()}get loggerFactory(){return this.core.loggerFactory}get transaction(){if(!this._transaction)throw new Error("Transaction undefined.");return this._transaction}cancel(e,t={}){if(!this.transaction)throw new Error("Transaction undefined.");if(!this.message.to)throw new Error("To undefined.");if(!this.message.from)throw new Error("From undefined.");const s=this.core.makeOutgoingRequestMessage(G.CANCEL,this.message.ruri,this.message.from.uri,this.message.to.uri,{toTag:this.message.toTag,fromTag:this.message.fromTag,callId:this.message.callId,cseq:this.message.cseq},t.extraHeaders);return s.branch=this.message.branch,this.message.headers.Route&&(s.headers.Route=this.message.headers.Route),e&&s.setHeader("Reason",e),this.transaction.state===de.Proceeding?new He(Ce,this.core,s):this.transaction.addStateChangeListener((()=>{this.transaction&&this.transaction.state===de.Proceeding&&new He(Ce,this.core,s)}),{once:!0}),s}authenticationGuard(e,t){const s=e.statusCode;if(!s)throw new Error("Response status code undefined.");if(401!==s&&407!==s)return!0;let i,r;if(401===s?(i=e.parseHeader("www-authenticate"),r="authorization"):(i=e.parseHeader("proxy-authenticate"),r="proxy-authorization"),!i)return this.logger.warn(s+" with wrong or missing challenge, cannot authenticate"),!0;if(this.challenged&&(this.stale||!0!==i.stale))return this.logger.warn(s+" apparently in authentication loop, cannot authenticate"),!0;if(!this.credentials&&(this.credentials=this.core.configuration.authenticationFactory(),!this.credentials))return this.logger.warn("Unable to obtain credentials, cannot authenticate"),!0;if(!this.credentials.authenticate(this.message,i))return!0;this.challenged=!0,i.stale&&(this.stale=!0);let n=this.message.cseq+=1;return t&&t.localSequenceNumber&&(t.incrementLocalSequenceNumber(),n=this.message.cseq=t.localSequenceNumber),this.message.setHeader("cseq",n+" "+this.message.method),this.message.setHeader(r,this.credentials.toString()),this.init(),!1}onRequestTimeout(){this.logger.warn("User agent client request timed out. Generating internal 408 Request Timeout.");const e=new x;e.statusCode=408,e.reasonPhrase="Request Timeout",this.receiveResponse(e)}onTransportError(e){this.logger.error(e.message),this.logger.error("User agent client request transport error. Generating internal 503 Service Unavailable.");const t=new x;t.statusCode=503,t.reasonPhrase="Service Unavailable",this.receiveResponse(t)}receiveResponse(e){if(!this.authenticationGuard(e))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e});break;case/^1[0-9]{2}$/.test(t):this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e});break;case/^2[0-9]{2}$/.test(t):this.delegate&&this.delegate.onAccept&&this.delegate.onAccept({message:e});break;case/^3[0-9]{2}$/.test(t):this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e});break;case/^[4-6][0-9]{2}$/.test(t):this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e});break;default:throw new Error(`Invalid status code ${t}`)}}init(){const e={loggerFactory:this.loggerFactory,onRequestTimeout:()=>this.onRequestTimeout(),onStateChange:e=>{e===de.Terminated&&(this.core.userAgentClients.delete(s),t===this._transaction&&this.dispose())},onTransportError:e=>this.onTransportError(e),receiveResponse:e=>this.receiveResponse(e)},t=new this.transactionConstructor(this.message,this.core.transport,e);this._transaction=t;const s=t.id+t.request.method;this.core.userAgentClients.set(s,this)}}class Pe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.BYE,s);super(Ce,e.userAgentCore,i,t),e.dispose()}}class xe extends Ee{constructor(e,t,s){super(e,t,s,de.Trying,"sip.transaction.nist")}dispose(){this.J&&(clearTimeout(this.J),this.J=void 0),super.dispose()}get kind(){return"nist"}receiveRequest(e){switch(this.state){case de.Trying:break;case de.Proceeding:if(!this.lastResponse)throw new Error("Last response undefined.");this.send(this.lastResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")}));break;case de.Completed:if(!this.lastResponse)throw new Error("Last response undefined.");this.send(this.lastResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of final response.")}));break;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}}receiveResponse(e,t){if(e<100||e>699)throw new Error(`Invalid status code ${e}`);if(e>100&&e<=199)throw new Error("Provisional response other than 100 not allowed.");switch(this.state){case de.Trying:if(this.lastResponse=t,e>=100&&e<200)return this.stateTransition(de.Proceeding),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send provisional response.")}));if(e>=200&&e<=699)return this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send final response.")}));break;case de.Proceeding:if(this.lastResponse=t,e>=200&&e<=699)return this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send final response.")}));break;case de.Completed:return;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`Non-INVITE server transaction received unexpected ${e} response from TU while in state ${this.state}.`;throw this.logger.error(s),new Error(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"non-INVITE server transaction"}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Trying:s();break;case de.Proceeding:this.state!==de.Trying&&s();break;case de.Completed:this.state!==de.Trying&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Proceeding&&this.state!==de.Completed&&(t||s());break;default:s()}e===de.Completed&&(this.J=setTimeout((()=>this.timerJ()),L.TIMER_J)),e===de.Terminated&&this.dispose(),this.setState(e)}timerJ(){this.logger.debug(`Timer J expired for NON-INVITE server transaction ${this.id}.`),this.state===de.Completed&&this.stateTransition(de.Terminated)}}class ke{constructor(e,t,s,i){this.transactionConstructor=e,this.core=t,this.message=s,this.delegate=i,this.logger=this.loggerFactory.getLogger("sip.user-agent-server"),this.toTag=s.toTag?s.toTag:C(),this.init()}dispose(){this.transaction.dispose()}get loggerFactory(){return this.core.loggerFactory}get transaction(){if(!this._transaction)throw new Error("Transaction undefined.");return this._transaction}accept(e={statusCode:200}){if(!this.acceptable)throw new B(`${this.message.method} not acceptable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<200||t>299)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}progress(e={statusCode:180}){if(!this.progressable)throw new B(`${this.message.method} not progressable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<101||t>199)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}redirect(e,t={statusCode:302}){if(!this.redirectable)throw new B(`${this.message.method} not redirectable in state ${this.transaction.state}.`);const s=t.statusCode;if(s<300||s>399)throw new TypeError(`Invalid statusCode: ${s}`);const i=new Array;e.forEach((e=>i.push(`Contact: ${e.toString()}`))),t.extraHeaders=(t.extraHeaders||[]).concat(i);return this.reply(t)}reject(e={statusCode:480}){if(!this.rejectable)throw new B(`${this.message.method} not rejectable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<400||t>699)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}trying(e){if(!this.tryingable)throw new B(`${this.message.method} not tryingable in state ${this.transaction.state}.`);return this.reply({statusCode:100})}receiveCancel(e){this.delegate&&this.delegate.onCancel&&this.delegate.onCancel(e)}get acceptable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding||this.transaction.state===de.Accepted;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get progressable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return!1;throw new Error("Unknown transaction type.")}get redirectable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get rejectable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get tryingable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying;throw new Error("Unknown transaction type.")}reply(e){e.toTag||100===e.statusCode||(e.toTag=this.toTag),e.userAgent=e.userAgent||this.core.configuration.userAgentHeaderFieldValue,e.supported=e.supported||this.core.configuration.supportedOptionTagsResponse;const t=Se(this.message,e);return this.transaction.receiveResponse(e.statusCode,t.message),t}init(){const e={loggerFactory:this.loggerFactory,onStateChange:e=>{e===de.Terminated&&(this.core.userAgentServers.delete(s),this.dispose())},onTransportError:e=>{this.logger.error(e.message),this.delegate&&this.delegate.onTransportError?this.delegate.onTransportError(e):this.logger.error("User agent server response transport error.")}},t=new this.transactionConstructor(this.message,this.core.transport,e);this._transaction=t;const s=t.id;this.core.userAgentServers.set(t.id,this)}}class _e extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}class qe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.INFO,s);super(Ce,e.userAgentCore,i,t)}}class Me extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}class Ne extends He{constructor(e,t,s){super(Ce,e,t,s)}}class Oe extends ke{constructor(e,t,s){super(xe,e,t,s)}}class Ue extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.NOTIFY,s);super(Ce,e.userAgentCore,i,t)}}class je extends ke{constructor(e,t,s){const i=void 0!==e.userAgentCore?e.userAgentCore:e;super(xe,i,t,s)}}class Fe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.PRACK,s);super(Ce,e.userAgentCore,i,t),e.signalingStateTransition(i)}}class Le extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s),e.signalingStateTransition(t),this.dialog=e}accept(e={statusCode:200}){return e.body&&this.dialog.signalingStateTransition(e.body),super.accept(e)}}class Be extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.INVITE,s);super(De,e.userAgentCore,i,t),this.delegate=t,e.signalingStateTransition(i),e.reinviteUserAgentClient=this,this.dialog=e}receiveResponse(e){if(!this.authenticationGuard(e,this.dialog))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e});break;case/^1[0-9]{2}$/.test(t):this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e,session:this.dialog,prack:e=>{throw new Error("Unimplemented.")}});break;case/^2[0-9]{2}$/.test(t):this.dialog.signalingStateTransition(e),this.delegate&&this.delegate.onAccept&&this.delegate.onAccept({message:e,session:this.dialog,ack:e=>this.dialog.ack(e)});break;case/^3[0-9]{2}$/.test(t):this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentClient=void 0,this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e});break;case/^[4-6][0-9]{2}$/.test(t):this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentClient=void 0,this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e});break;default:throw new Error(`Invalid status code ${t}`)}}}class Ge extends ke{constructor(e,t,s){super($e,e.userAgentCore,t,s),e.reinviteUserAgentServer=this,this.dialog=e}accept(e={statusCode:200}){e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(this.dialog.routeSet.map((e=>`Record-Route: ${e}`)));const t=super.accept(e),s=this.dialog,i=Object.assign(Object.assign({},t),{session:s});return e.body&&this.dialog.signalingStateTransition(e.body),this.dialog.reConfirm(),i}progress(e={statusCode:180}){const t=super.progress(e),s=this.dialog,i=Object.assign(Object.assign({},t),{session:s});return e.body&&this.dialog.signalingStateTransition(e.body),i}redirect(e,t={statusCode:302}){throw this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentServer=void 0,new Error("Unimplemented.")}reject(e={statusCode:488}){return this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentServer=void 0,super.reject(e)}}class Ve extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.REFER,s);super(Ce,e.userAgentCore,i,t)}}class We extends ke{constructor(e,t,s){const i=void 0!==e.userAgentCore?e.userAgentCore:e;super(xe,i,t,s)}}class Ke extends Ae{constructor(e,t,s,i){super(t,s),this.initialTransaction=e,this._signalingState=U.Initial,this.ackWait=!1,this.ackProcessing=!1,this.delegate=i,e instanceof $e&&(this.ackWait=!0),this.early||this.start2xxRetransmissionTimer(),this.signalingStateTransition(e.request),this.logger=t.loggerFactory.getLogger("sip.invite-dialog"),this.logger.log(`INVITE dialog ${this.id} constructed`)}dispose(){super.dispose(),this._signalingState=U.Closed,this._offer=void 0,this._answer=void 0,this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.logger.log(`INVITE dialog ${this.id} destroyed`)}get sessionState(){return this.early?O.Early:this.ackWait?O.AckWait:this._signalingState===U.Closed?O.Terminated:O.Confirmed}get signalingState(){return this._signalingState}get offer(){return this._offer}get answer(){return this._answer}confirm(){this.early&&this.start2xxRetransmissionTimer(),super.confirm()}reConfirm(){this.reinviteUserAgentServer&&this.startReInvite2xxRetransmissionTimer()}ack(e={}){let t;if(this.logger.log(`INVITE dialog ${this.id} sending ACK request`),this.reinviteUserAgentClient){if(!(this.reinviteUserAgentClient.transaction instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");t=this.reinviteUserAgentClient.transaction,this.reinviteUserAgentClient=void 0}else{if(!(this.initialTransaction instanceof De))throw new Error("Initial transaction not instance of InviteClientTransaction.");t=this.initialTransaction}const s=this.createOutgoingRequestMessage(G.ACK,{cseq:t.request.cseq,extraHeaders:e.extraHeaders,body:e.body});return t.ackResponse(s),this.signalingStateTransition(s),{message:s}}bye(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending BYE request`),this.initialTransaction instanceof $e){if(this.early)throw new Error("UAS MUST NOT send a BYE on early dialogs.");if(this.ackWait&&this.initialTransaction.state!==de.Terminated)throw new Error("UAS MUST NOT send a BYE on a confirmed dialog until it has received an ACK for its 2xx response or until the server transaction times out.")}return new Pe(this,e,t)}info(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending INFO request`),this.early)throw new Error("Dialog not confirmed.");return new qe(this,e,t)}invite(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending INVITE request`),this.early)throw new Error("Dialog not confirmed.");if(this.reinviteUserAgentClient)throw new Error("There is an ongoing re-INVITE client transaction.");if(this.reinviteUserAgentServer)throw new Error("There is an ongoing re-INVITE server transaction.");return new Be(this,e,t)}message(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending MESSAGE request`),this.early)throw new Error("Dialog not confirmed.");const s=this.createOutgoingRequestMessage(G.MESSAGE,t);return new Ne(this.core,s,e)}notify(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending NOTIFY request`),this.early)throw new Error("Dialog not confirmed.");return new Ue(this,e,t)}prack(e,t){return this.logger.log(`INVITE dialog ${this.id} sending PRACK request`),new Fe(this,e,t)}refer(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending REFER request`),this.early)throw new Error("Dialog not confirmed.");return new Ve(this,e,t)}receiveRequest(e){if(this.logger.log(`INVITE dialog ${this.id} received ${e.method} request`),e.method!==G.ACK)if(this.sequenceGuard(e)){if(super.receiveRequest(e),e.method===G.INVITE){const t=()=>{const e=this.ackWait?"waiting for initial ACK":"processing initial ACK";this.logger.warn(`INVITE dialog ${this.id} received re-INVITE while ${e}`);let t="RFC 5407 suggests the following to avoid this race condition... ";t+=" Note: Implementation issues are outside the scope of this document,",t+=" but the following tip is provided for avoiding race conditions of",t+=" this type. The caller can delay sending re-INVITE F6 for some period",t+=" of time (2 seconds, perhaps), after which the caller can reasonably",t+=" assume that its ACK has been received. Implementors can decouple the",t+=" actions of the user (e.g., pressing the hold button) from the actions",t+=" of the protocol (the sending of re-INVITE F6), so that the UA can",t+=" behave like this. In this case, it is the implementor's choice as to",t+=" how long to wait. In most cases, such an implementation may be",t+=" useful to prevent the type of race condition shown in this section.",t+=" This document expresses no preference about whether or not they",t+=" should wait for an ACK to be delivered. After considering the impact",t+=" on user experience, implementors should decide whether or not to wait",t+=" for a while, because the user experience depends on the",t+=" implementation and has no direct bearing on protocol behavior.",this.logger.warn("RFC 5407 suggests the following to avoid this race condition... Note: Implementation issues are outside the scope of this document, but the following tip is provided for avoiding race conditions of this type. The caller can delay sending re-INVITE F6 for some period of time (2 seconds, perhaps), after which the caller can reasonably assume that its ACK has been received. Implementors can decouple the actions of the user (e.g., pressing the hold button) from the actions of the protocol (the sending of re-INVITE F6), so that the UA can behave like this. In this case, it is the implementor's choice as to how long to wait. In most cases, such an implementation may be useful to prevent the type of race condition shown in this section. This document expresses no preference about whether or not they should wait for an ACK to be delivered. After considering the impact on user experience, implementors should decide whether or not to wait for a while, because the user experience depends on the implementation and has no direct bearing on protocol behavior.")},s=[`Retry-After: ${Math.floor(10*Math.random())+1}`];if(this.ackProcessing)return this.core.replyStateless(e,{statusCode:500,extraHeaders:s}),void t();if(this.ackWait&&this.signalingState!==U.Stable)return this.core.replyStateless(e,{statusCode:500,extraHeaders:s}),void t();if(this.reinviteUserAgentServer)return void this.core.replyStateless(e,{statusCode:500,extraHeaders:s});if(this.reinviteUserAgentClient)return void this.core.replyStateless(e,{statusCode:491})}if(e.method===G.INVITE){const t=e.parseHeader("contact");if(!t)throw new Error("Contact undefined.");if(!(t instanceof v))throw new Error("Contact not instance of NameAddrHeader.");this.dialogState.remoteTarget=t.uri}switch(e.method){case G.BYE:{const t=new _e(this,e);this.delegate&&this.delegate.onBye?this.delegate.onBye(t):t.accept(),this.dispose()}break;case G.INFO:{const t=new Me(this,e);this.delegate&&this.delegate.onInfo?this.delegate.onInfo(t):t.reject({statusCode:469,extraHeaders:["Recv-Info:"]})}break;case G.INVITE:{const t=new Ge(this,e);this.signalingStateTransition(e),this.delegate&&this.delegate.onInvite?this.delegate.onInvite(t):t.reject({statusCode:488})}break;case G.MESSAGE:{const t=new Oe(this.core,e);this.delegate&&this.delegate.onMessage?this.delegate.onMessage(t):t.accept()}break;case G.NOTIFY:{const t=new je(this,e);this.delegate&&this.delegate.onNotify?this.delegate.onNotify(t):t.accept()}break;case G.PRACK:{const t=new Le(this,e);this.delegate&&this.delegate.onPrack?this.delegate.onPrack(t):t.accept()}break;case G.REFER:{const t=new We(this,e);this.delegate&&this.delegate.onRefer?this.delegate.onRefer(t):t.reject()}break;default:this.logger.log(`INVITE dialog ${this.id} received unimplemented ${e.method} request`),this.core.replyStateless(e,{statusCode:501})}}else this.logger.log(`INVITE dialog ${this.id} rejected out of order ${e.method} request.`);else{if(this.ackWait){if(this.initialTransaction instanceof De)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);if(this.initialTransaction.request.cseq!==e.cseq)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);this.ackWait=!1}else{if(!this.reinviteUserAgentServer)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);if(this.reinviteUserAgentServer.transaction.request.cseq!==e.cseq)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);this.reinviteUserAgentServer=void 0}if(this.signalingStateTransition(e),this.delegate&&this.delegate.onAck){const t=this.delegate.onAck({message:e});t instanceof Promise&&(this.ackProcessing=!0,t.then((()=>this.ackProcessing=!1)).catch((()=>this.ackProcessing=!1)))}}}reliableSequenceGuard(e){const t=e.statusCode;if(!t)throw new Error("Status code undefined");if(t>100&&t<200){const t=e.getHeader("require"),s=e.getHeader("rseq"),i=t&&t.includes("100rel")&&s?Number(s):void 0;if(i){if(this.rseq&&this.rseq+1!==i)return!1;this.rseq=this.rseq?this.rseq+1:i}}return!0}signalingStateRollback(){this._signalingState!==U.HaveLocalOffer&&this.signalingState!==U.HaveRemoteOffer||this._rollbackOffer&&this._rollbackAnswer&&(this._signalingState=U.Stable,this._offer=this._rollbackOffer,this._answer=this._rollbackAnswer)}signalingStateTransition(e){const t=N(e);if(t&&"session"===t.contentDisposition){if(this._signalingState===U.Stable&&(this._rollbackOffer=this._offer,this._rollbackAnswer=this._answer),e instanceof P)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveRemoteOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:this._signalingState=U.Stable,this._answer=t;break;case U.HaveRemoteOffer:case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(e instanceof x)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveRemoteOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:this._signalingState=U.Stable,this._answer=t;break;case U.HaveRemoteOffer:case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(e instanceof k)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveLocalOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:break;case U.HaveRemoteOffer:this._signalingState=U.Stable,this._answer=t;break;case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(M(e))switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveLocalOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:break;case U.HaveRemoteOffer:this._signalingState=U.Stable,this._answer=t;break;case U.Closed:break;default:throw new Error("Unexpected signaling state.")}}}start2xxRetransmissionTimer(){if(this.initialTransaction instanceof $e){const e=this.initialTransaction;let t=L.T1;const s=()=>{this.ackWait?(this.logger.log("No ACK for 2xx response received, attempting retransmission"),e.retransmitAcceptedResponse(),t=Math.min(2*t,L.T2),this.invite2xxTimer=setTimeout(s,t)):this.invite2xxTimer=void 0};this.invite2xxTimer=setTimeout(s,t);const i=()=>{e.state===de.Terminated&&(e.removeStateChangeListener(i),this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.ackWait&&(this.delegate&&this.delegate.onAckTimeout?this.delegate.onAckTimeout():this.bye()))};e.addStateChangeListener(i)}}startReInvite2xxRetransmissionTimer(){if(this.reinviteUserAgentServer&&this.reinviteUserAgentServer.transaction instanceof $e){const e=this.reinviteUserAgentServer.transaction;let t=L.T1;const s=()=>{this.reinviteUserAgentServer?(this.logger.log("No ACK for 2xx response received, attempting retransmission"),e.retransmitAcceptedResponse(),t=Math.min(2*t,L.T2),this.invite2xxTimer=setTimeout(s,t)):this.invite2xxTimer=void 0};this.invite2xxTimer=setTimeout(s,t);const i=()=>{e.state===de.Terminated&&(e.removeStateChangeListener(i),this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.reinviteUserAgentServer)};e.addStateChangeListener(i)}}}class Ye extends He{constructor(e,t,s){super(De,e,t,s),this.confirmedDialogAcks=new Map,this.confirmedDialogs=new Map,this.earlyDialogs=new Map,this.delegate=s}dispose(){this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),super.dispose()}onTransportError(e){if(this.transaction.state===de.Calling)return super.onTransportError(e);this.logger.error(e.message),this.logger.error("User agent client request transport error while sending ACK.")}receiveResponse(e){if(!this.authenticationGuard(e))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):return void(this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e}));case/^1[0-9]{2}$/.test(t):{if(!e.toTag)return void this.logger.warn("Non-100 1xx INVITE response received without a to tag, dropping.");if(!e.parseHeader("contact"))return void this.logger.error("Non-100 1xx INVITE response received without a Contact header field, dropping.");const t=Ae.initialDialogStateForUserAgentClient(this.message,e);let s=this.earlyDialogs.get(t.id);if(!s){const e=this.transaction;if(!(e instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");s=new Ke(e,this.core,t),this.earlyDialogs.set(s.id,s)}if(!s.reliableSequenceGuard(e))return void this.logger.warn("1xx INVITE reliable response received out of order or is a retransmission, dropping.");s.signalingState!==U.Initial&&s.signalingState!==U.HaveLocalOffer||s.signalingStateTransition(e);const i=s;this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e,session:i,prack:e=>i.prack(void 0,e)})}return;case/^2[0-9]{2}$/.test(t):{if(!e.toTag)return void this.logger.error("2xx INVITE response received without a to tag, dropping.");if(!e.parseHeader("contact"))return void this.logger.error("2xx INVITE response received without a Contact header field, dropping.");const t=Ae.initialDialogStateForUserAgentClient(this.message,e);let s=this.confirmedDialogs.get(t.id);if(s){const e=this.confirmedDialogAcks.get(t.id);if(e){const t=this.transaction;if(!(t instanceof De))throw new Error("Client transaction not instance of InviteClientTransaction.");t.ackResponse(e.message)}return}if(s=this.earlyDialogs.get(t.id),s)s.confirm(),s.recomputeRouteSet(e),this.earlyDialogs.delete(s.id),this.confirmedDialogs.set(s.id,s);else{const e=this.transaction;if(!(e instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");s=new Ke(e,this.core,t),this.confirmedDialogs.set(s.id,s)}s.signalingState!==U.Initial&&s.signalingState!==U.HaveLocalOffer||s.signalingStateTransition(e);const i=s;if(this.delegate&&this.delegate.onAccept)this.delegate.onAccept({message:e,session:i,ack:e=>{const t=i.ack(e);return this.confirmedDialogAcks.set(i.id,t),t}});else{const e=i.ack();this.confirmedDialogAcks.set(i.id,e)}}return;case/^3[0-9]{2}$/.test(t):return this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),void(this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e}));case/^[4-6][0-9]{2}$/.test(t):return this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),void(this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e}));default:throw new Error(`Invalid status code ${t}`)}throw new Error(`Executing what should be an unreachable code path receiving ${t} response.`)}}class Ze extends ke{constructor(e,t,s){super($e,e,t,s),this.core=e}dispose(){this.earlyDialog&&this.earlyDialog.dispose(),super.dispose()}accept(e={statusCode:200}){if(!this.acceptable)throw new B(`${this.message.method} not acceptable in state ${this.transaction.state}.`);if(!this.confirmedDialog)if(this.earlyDialog)this.earlyDialog.confirm(),this.confirmedDialog=this.earlyDialog,this.earlyDialog=void 0;else{const e=this.transaction;if(!(e instanceof $e))throw new Error("Transaction not instance of InviteClientTransaction.");const t=Ae.initialDialogStateForUserAgentServer(this.message,this.toTag);this.confirmedDialog=new Ke(e,this.core,t)}const t=this.message.getHeaders("record-route").map((e=>`Record-Route: ${e}`)),s=`Contact: ${this.core.configuration.contact.toString()}`,i="Allow: "+V.toString();if(!e.body)if(this.confirmedDialog.signalingState===U.Stable)e.body=this.confirmedDialog.answer;else if(this.confirmedDialog.signalingState===U.Initial||this.confirmedDialog.signalingState===U.HaveRemoteOffer)throw new Error("Response must have a body.");e.statusCode=e.statusCode||200,e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(t),e.extraHeaders.push(i),e.extraHeaders.push(s);const r=super.accept(e),n=this.confirmedDialog,o=Object.assign(Object.assign({},r),{session:n});return e.body&&this.confirmedDialog.signalingState!==U.Stable&&this.confirmedDialog.signalingStateTransition(e.body),o}progress(e={statusCode:180}){if(!this.progressable)throw new B(`${this.message.method} not progressable in state ${this.transaction.state}.`);if(!this.earlyDialog){const e=this.transaction;if(!(e instanceof $e))throw new Error("Transaction not instance of InviteClientTransaction.");const t=Ae.initialDialogStateForUserAgentServer(this.message,this.toTag,!0);this.earlyDialog=new Ke(e,this.core,t)}const t=this.message.getHeaders("record-route").map((e=>`Record-Route: ${e}`)),s=`Contact: ${this.core.configuration.contact}`;e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(t),e.extraHeaders.push(s);const i=super.progress(e),r=this.earlyDialog,n=Object.assign(Object.assign({},i),{session:r});return e.body&&this.earlyDialog.signalingState!==U.Stable&&this.earlyDialog.signalingStateTransition(e.body),n}redirect(e,t={statusCode:302}){return super.redirect(e,t)}reject(e={statusCode:486}){return super.reject(e)}}class Je extends He{constructor(e,t,s){super(Ce,e,t,s)}}class ze extends He{constructor(e,t,s){super(Ce,e,t,s)}}class Xe extends ke{constructor(e,t,s){super(xe,e,t,s),this.core=e}}class Qe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.SUBSCRIBE,s);super(Ce,e.userAgentCore,i,t),this.dialog=e}waitNotifyStop(){}receiveResponse(e){if(e.statusCode&&e.statusCode>=200&&e.statusCode<300){const t=e.getHeader("Expires");if(t){const e=Number(t);this.dialog.subscriptionExpires>e&&(this.dialog.subscriptionExpires=e)}else this.logger.warn("Expires header missing in a 200-class response to SUBSCRIBE")}if(e.statusCode&&e.statusCode>=400&&e.statusCode<700){[404,405,410,416,480,481,482,483,484,485,489,501,604].includes(e.statusCode)&&this.dialog.terminate()}super.receiveResponse(e)}}class et extends Ae{constructor(e,t,s,i,r,n){super(i,r),this.delegate=n,this._autoRefresh=!1,this._subscriptionEvent=e,this._subscriptionExpires=t,this._subscriptionExpiresInitial=t,this._subscriptionExpiresLastSet=Math.floor(Date.now()/1e3),this._subscriptionRefresh=void 0,this._subscriptionRefreshLastSet=void 0,this._subscriptionState=s,this.logger=i.loggerFactory.getLogger("sip.subscribe-dialog"),this.logger.log(`SUBSCRIBE dialog ${this.id} constructed`)}static initialDialogStateForSubscription(e,t){const s=t.getHeaders("record-route"),i=t.parseHeader("contact");if(!i)throw new Error("Contact undefined.");if(!(i instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const r=i.uri,n=e.cseq,o=e.callId,a=e.fromTag,c=t.fromTag;if(!o)throw new Error("Call id undefined.");if(!a)throw new Error("From tag undefined.");if(!c)throw new Error("To tag undefined.");if(!e.from)throw new Error("From undefined.");if(!e.to)throw new Error("To undefined.");return{id:o+a+c,early:!1,callId:o,localTag:a,remoteTag:c,localSequenceNumber:n,remoteSequenceNumber:undefined,localURI:e.from.uri,remoteURI:e.to.uri,remoteTarget:r,routeSet:s,secure:!1}}dispose(){super.dispose(),this.N&&(clearTimeout(this.N),this.N=void 0),this.refreshTimerClear(),this.logger.log(`SUBSCRIBE dialog ${this.id} destroyed`)}get autoRefresh(){return this._autoRefresh}set autoRefresh(e){this._autoRefresh=!0,this.refreshTimerSet()}get subscriptionEvent(){return this._subscriptionEvent}get subscriptionExpires(){const e=Math.floor(Date.now()/1e3)-this._subscriptionExpiresLastSet,t=this._subscriptionExpires-e;return Math.max(t,0)}set subscriptionExpires(e){if(e<0)throw new Error("Expires must be greater than or equal to zero.");if(this._subscriptionExpires=e,this._subscriptionExpiresLastSet=Math.floor(Date.now()/1e3),this.autoRefresh){const t=this.subscriptionRefresh;(void 0===t||t>=e)&&this.refreshTimerSet()}}get subscriptionExpiresInitial(){return this._subscriptionExpiresInitial}get subscriptionRefresh(){if(void 0===this._subscriptionRefresh||void 0===this._subscriptionRefreshLastSet)return;const e=Math.floor(Date.now()/1e3)-this._subscriptionRefreshLastSet,t=this._subscriptionRefresh-e;return Math.max(t,0)}get subscriptionState(){return this._subscriptionState}receiveRequest(e){if(this.logger.log(`SUBSCRIBE dialog ${this.id} received ${e.method} request`),this.sequenceGuard(e))if(super.receiveRequest(e),e.method===G.NOTIFY)this.onNotify(e);else this.logger.log(`SUBSCRIBE dialog ${this.id} received unimplemented ${e.method} request`),this.core.replyStateless(e,{statusCode:501});else this.logger.log(`SUBSCRIBE dialog ${this.id} rejected out of order ${e.method} request.`)}refresh(){const e="Allow: "+V.toString(),t={};return t.extraHeaders=(t.extraHeaders||[]).slice(),t.extraHeaders.push(e),t.extraHeaders.push("Event: "+this.subscriptionEvent),t.extraHeaders.push("Expires: "+this.subscriptionExpiresInitial),t.extraHeaders.push("Contact: "+this.core.configuration.contact.toString()),this.subscribe(void 0,t)}subscribe(e,t={}){var s;if(this.subscriptionState!==re.Pending&&this.subscriptionState!==re.Active)throw new Error(`Invalid state ${this.subscriptionState}. May only re-subscribe while in state "pending" or "active".`);this.logger.log(`SUBSCRIBE dialog ${this.id} sending SUBSCRIBE request`);const i=new Qe(this,e,t);return this.N&&(clearTimeout(this.N),this.N=void 0),(null===(s=t.extraHeaders)||void 0===s?void 0:s.includes("Expires: 0"))||(this.N=setTimeout((()=>this.timerN()),L.TIMER_N)),i}terminate(){this.stateTransition(re.Terminated),this.onTerminated()}unsubscribe(){const e="Allow: "+V.toString(),t={};return t.extraHeaders=(t.extraHeaders||[]).slice(),t.extraHeaders.push(e),t.extraHeaders.push("Event: "+this.subscriptionEvent),t.extraHeaders.push("Expires: 0"),t.extraHeaders.push("Contact: "+this.core.configuration.contact.toString()),this.subscribe(void 0,t)}onNotify(e){const t=e.parseHeader("Event").event;if(!t||t!==this.subscriptionEvent)return void this.core.replyStateless(e,{statusCode:489});this.N&&(clearTimeout(this.N),this.N=void 0);const s=e.parseHeader("Subscription-State");if(!s||!s.state)return void this.core.replyStateless(e,{statusCode:489});const i=s.state,r=s.expires?Math.max(s.expires,0):void 0;switch(i){case"pending":this.stateTransition(re.Pending,r);break;case"active":this.stateTransition(re.Active,r);break;case"terminated":this.stateTransition(re.Terminated,r);break;default:this.logger.warn("Unrecognized subscription state.")}const n=new je(this,e);this.delegate&&this.delegate.onNotify?this.delegate.onNotify(n):n.accept()}onRefresh(e){this.delegate&&this.delegate.onRefresh&&this.delegate.onRefresh(e)}onTerminated(){this.delegate&&this.delegate.onTerminated&&this.delegate.onTerminated()}refreshTimerClear(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0)}refreshTimerSet(){if(this.refreshTimerClear(),this.autoRefresh&&this.subscriptionExpires>0){const e=900*this.subscriptionExpires;this._subscriptionRefresh=Math.floor(e/1e3),this._subscriptionRefreshLastSet=Math.floor(Date.now()/1e3),this.refreshTimer=setTimeout((()=>{this.refreshTimer=void 0,this._subscriptionRefresh=void 0,this._subscriptionRefreshLastSet=void 0,this.onRefresh(this.refresh())}),e)}}stateTransition(e,t){const s=()=>{this.logger.warn(`Invalid subscription state transition from ${this.subscriptionState} to ${e}`)};switch(e){case re.Initial:case re.NotifyWait:return void s();case re.Pending:if(this.subscriptionState!==re.NotifyWait&&this.subscriptionState!==re.Pending)return void s();break;case re.Active:case re.Terminated:if(this.subscriptionState!==re.NotifyWait&&this.subscriptionState!==re.Pending&&this.subscriptionState!==re.Active)return void s();break;default:return void s()}e===re.Pending&&t&&(this.subscriptionExpires=t),e===re.Active&&t&&(this.subscriptionExpires=t),e===re.Terminated&&this.dispose(),this._subscriptionState=e}timerN(){this.logger.warn("Timer N expired for SUBSCRIBE dialog. Timed out waiting for NOTIFY."),this.subscriptionState!==re.Terminated&&(this.stateTransition(re.Terminated),this.onTerminated())}}class tt extends He{constructor(e,t,s){const i=t.getHeader("Event");if(!i)throw new Error("Event undefined");const r=t.getHeader("Expires");if(!r)throw new Error("Expires undefined");super(Ce,e,t,s),this.delegate=s,this.subscriberId=t.callId+t.fromTag+i,this.subscriptionExpiresRequested=this.subscriptionExpires=Number(r),this.subscriptionEvent=i,this.subscriptionState=re.NotifyWait,this.waitNotifyStart()}dispose(){super.dispose()}onNotify(e){const t=e.message.parseHeader("Event").event;if(!t||t!==this.subscriptionEvent)return this.logger.warn("Failed to parse event."),void e.reject({statusCode:489});const s=e.message.parseHeader("Subscription-State");if(!s||!s.state)return this.logger.warn("Failed to parse subscription state."),void e.reject({statusCode:489});const i=s.state;switch(i){case"pending":case"active":case"terminated":break;default:return this.logger.warn(`Invalid subscription state ${i}`),void e.reject({statusCode:489})}if("terminated"!==i){if(!e.message.parseHeader("contact"))return this.logger.warn("Failed to parse contact."),void e.reject({statusCode:489})}if(this.dialog)throw new Error("Dialog already created. This implementation only supports install of single subscriptions.");switch(this.waitNotifyStop(),this.subscriptionExpires=s.expires?Math.min(this.subscriptionExpires,Math.max(s.expires,0)):this.subscriptionExpires,i){case"pending":this.subscriptionState=re.Pending;break;case"active":this.subscriptionState=re.Active;break;case"terminated":this.subscriptionState=re.Terminated;break;default:throw new Error(`Unrecognized state ${i}.`)}if(this.subscriptionState!==re.Terminated){const t=et.initialDialogStateForSubscription(this.message,e.message);this.dialog=new et(this.subscriptionEvent,this.subscriptionExpires,this.subscriptionState,this.core,t)}if(this.delegate&&this.delegate.onNotify){const t=e,s=this.dialog;this.delegate.onNotify({request:t,subscription:s})}else e.accept()}waitNotifyStart(){this.N||(this.core.subscribers.set(this.subscriberId,this),this.N=setTimeout((()=>this.timerN()),L.TIMER_N))}waitNotifyStop(){this.N&&(this.core.subscribers.delete(this.subscriberId),clearTimeout(this.N),this.N=void 0)}receiveResponse(e){if(this.authenticationGuard(e)){if(e.statusCode&&e.statusCode>=200&&e.statusCode<300){const t=e.getHeader("Expires");if(t){const e=Number(t);e>this.subscriptionExpiresRequested&&this.logger.warn("Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request"),ethis.subscriptionExpires&&(this.dialog.subscriptionExpires=this.subscriptionExpires)}e.statusCode&&e.statusCode>=300&&e.statusCode<700&&this.waitNotifyStop(),super.receiveResponse(e)}}timerN(){this.logger.warn("Timer N expired for SUBSCRIBE user agent client. Timed out waiting for NOTIFY."),this.waitNotifyStop(),this.delegate&&this.delegate.onNotifyTimeout&&this.delegate.onNotifyTimeout()}}class st extends ke{constructor(e,t,s){super(xe,e,t,s),this.core=e}}const it=["application/sdp","application/dtmf-relay"];class rt{constructor(e,t={}){this.userAgentClients=new Map,this.userAgentServers=new Map,this.configuration=e,this.delegate=t,this.dialogs=new Map,this.subscribers=new Map,this.logger=e.loggerFactory.getLogger("sip.user-agent-core")}dispose(){this.reset()}reset(){this.dialogs.forEach((e=>e.dispose())),this.dialogs.clear(),this.subscribers.forEach((e=>e.dispose())),this.subscribers.clear(),this.userAgentClients.forEach((e=>e.dispose())),this.userAgentClients.clear(),this.userAgentServers.forEach((e=>e.dispose())),this.userAgentServers.clear()}get loggerFactory(){return this.configuration.loggerFactory}get transport(){const e=this.configuration.transportAccessor();if(!e)throw new Error("Transport undefined.");return e}invite(e,t){return new Ye(this,e,t)}message(e,t){return new Ne(this,e,t)}publish(e,t){return new Je(this,e,t)}register(e,t){return new ze(this,e,t)}subscribe(e,t){return new tt(this,e,t)}request(e,t){return new He(Ce,this,e,t)}makeOutgoingRequestMessage(e,t,s,i,r,n,o){const a=this.configuration.sipjsId,c=this.configuration.displayName,h=this.configuration.viaForceRport,d=this.configuration.hackViaTcp,l=this.configuration.supportedOptionTags.slice();e===G.REGISTER&&l.push("path","gruu"),e===G.INVITE&&(this.configuration.contact.pubGruu||this.configuration.contact.tempGruu)&&l.push("gruu");const g={callIdPrefix:a,forceRport:h,fromDisplayName:c,hackViaTcp:d,optionTags:l,routeSet:this.configuration.routeSet,userAgentString:this.configuration.userAgentHeaderFieldValue,viaHost:this.configuration.viaHost},u=Object.assign(Object.assign({},g),r);return new k(e,t,s,i,u,n,o)}receiveIncomingRequestFromTransport(e){this.receiveRequestFromTransport(e)}receiveIncomingResponseFromTransport(e){this.receiveResponseFromTransport(e)}replyStateless(e,t){const s=this.configuration.userAgentHeaderFieldValue,i=this.configuration.supportedOptionTagsResponse;t=Object.assign(Object.assign({},t),{userAgent:s,supported:i});const r=Se(e,t);return this.transport.send(r.message).catch((t=>{t instanceof Error&&this.logger.error(t.message),this.logger.error(`Transport error occurred sending stateless reply to ${e.method} request.`)})),r}receiveRequestFromTransport(e){const t=e.viaBranch,s=this.userAgentServers.get(t);e.method===G.ACK&&s&&s.transaction.state===de.Accepted&&s instanceof Ze?this.logger.warn(`Discarding out of dialog ACK after 2xx response sent on transaction ${t}.`):e.method!==G.CANCEL?s?s.transaction.receiveRequest(e):this.receiveRequest(e):s?(this.replyStateless(e,{statusCode:200}),s.transaction instanceof $e&&s.transaction.state===de.Proceeding&&s instanceof Ze&&s.receiveCancel(e)):this.replyStateless(e,{statusCode:481})}receiveRequest(e){if(!V.includes(e.method)){const t="Allow: "+V.toString();return void this.replyStateless(e,{statusCode:405,extraHeaders:[t]})}if(!e.ruri)throw new Error("Request-URI undefined.");if("sip"!==e.ruri.scheme)return void this.replyStateless(e,{statusCode:416});const t=e.ruri,s=e=>!!e&&e.user===t.user;if(!s(this.configuration.aor)&&!(s(this.configuration.contact.uri)||s(this.configuration.contact.pubGruu)||s(this.configuration.contact.tempGruu)))return this.logger.warn("Request-URI does not point to us."),void(e.method!==G.ACK&&this.replyStateless(e,{statusCode:404}));if(e.method!==G.INVITE||e.hasHeader("Contact")){if(!e.toTag){const t=e.viaBranch;if(!this.userAgentServers.has(t)){if(Array.from(this.userAgentServers.values()).some((t=>t.transaction.request.fromTag===e.fromTag&&t.transaction.request.callId===e.callId&&t.transaction.request.cseq===e.cseq)))return void this.replyStateless(e,{statusCode:482})}}e.toTag?this.receiveInsideDialogRequest(e):this.receiveOutsideDialogRequest(e)}else this.replyStateless(e,{statusCode:400,reasonPhrase:"Missing Contact Header"})}receiveInsideDialogRequest(e){if(e.method===G.NOTIFY){const t=e.parseHeader("Event");if(!t||!t.event)return void this.replyStateless(e,{statusCode:489});const s=e.callId+e.toTag+t.event,i=this.subscribers.get(s);if(i){const t=new je(this,e);return void i.onNotify(t)}}const t=e.callId+e.toTag+e.fromTag,s=this.dialogs.get(t);if(s){if(e.method===G.OPTIONS){const t="Allow: "+V.toString(),s="Accept: "+it.toString();return void this.replyStateless(e,{statusCode:200,extraHeaders:[t,s]})}s.receiveRequest(e)}else e.method!==G.ACK&&this.replyStateless(e,{statusCode:481})}receiveOutsideDialogRequest(e){switch(e.method){case G.ACK:break;case G.BYE:this.replyStateless(e,{statusCode:481});break;case G.CANCEL:throw new Error(`Unexpected out of dialog request method ${e.method}.`);case G.INFO:this.replyStateless(e,{statusCode:405});break;case G.INVITE:{const t=new Ze(this,e);this.delegate.onInvite?this.delegate.onInvite(t):t.reject()}break;case G.MESSAGE:{const t=new Oe(this,e);this.delegate.onMessage?this.delegate.onMessage(t):t.accept()}break;case G.NOTIFY:{const t=new je(this,e);this.delegate.onNotify?this.delegate.onNotify(t):t.reject({statusCode:405})}break;case G.OPTIONS:{const t="Allow: "+V.toString(),s="Accept: "+it.toString();this.replyStateless(e,{statusCode:200,extraHeaders:[t,s]})}break;case G.REFER:{const t=new We(this,e);this.delegate.onRefer?this.delegate.onRefer(t):t.reject({statusCode:405})}break;case G.REGISTER:{const t=new Xe(this,e);this.delegate.onRegister?this.delegate.onRegister(t):t.reject({statusCode:405})}break;case G.SUBSCRIBE:{const t=new st(this,e);this.delegate.onSubscribe?this.delegate.onSubscribe(t):t.reject({statusCode:480})}break;default:throw new Error(`Unexpected out of dialog request method ${e.method}.`)}}receiveResponseFromTransport(e){if(e.getHeaders("via").length>1)return void this.logger.warn("More than one Via header field present in the response, dropping");const t=e.viaBranch+e.method,s=this.userAgentClients.get(t);s?s.transaction.receiveResponse(e):this.logger.warn(`Discarding unmatched ${e.statusCode} response to ${e.method} ${t}.`)}}function nt(){return e=>e.audio||e.video?void 0===navigator.mediaDevices?Promise.reject(new Error("Media devices not available in insecure contexts.")):navigator.mediaDevices.getUserMedia.call(navigator.mediaDevices,e):Promise.resolve(new MediaStream)}function ot(){return{bundlePolicy:"balanced",certificates:void 0,iceCandidatePoolSize:0,iceServers:[{urls:"stun:stun.l.google.com:19302"}],iceTransportPolicy:"all",rtcpMuxPolicy:"require"}}class at{constructor(e,t,s){e.debug("SessionDescriptionHandler.constructor"),this.logger=e,this.mediaStreamFactory=t,this.sessionDescriptionHandlerConfiguration=s,this._localMediaStream=new MediaStream,this._remoteMediaStream=new MediaStream,this._peerConnection=new RTCPeerConnection(null==s?void 0:s.peerConnectionConfiguration),this.initPeerConnectionEventHandlers()}get localMediaStream(){return this._localMediaStream}get remoteMediaStream(){return this._remoteMediaStream}get dataChannel(){return this._dataChannel}get peerConnection(){return this._peerConnection}get peerConnectionDelegate(){return this._peerConnectionDelegate}set peerConnectionDelegate(e){this._peerConnectionDelegate=e}static dispatchAddTrackEvent(e,t){e.dispatchEvent(new MediaStreamTrackEvent("addtrack",{track:t}))}static dispatchRemoveTrackEvent(e,t){e.dispatchEvent(new MediaStreamTrackEvent("removetrack",{track:t}))}close(){this.logger.debug("SessionDescriptionHandler.close"),void 0!==this._peerConnection&&(this._peerConnection.getReceivers().forEach((e=>{e.track&&e.track.stop()})),this._peerConnection.getSenders().forEach((e=>{e.track&&e.track.stop()})),this._dataChannel&&this._dataChannel.close(),this._peerConnection.close(),this._peerConnection=void 0)}enableReceiverTracks(e){const t=this.peerConnection;if(!t)throw new Error("Peer connection closed.");t.getReceivers().forEach((t=>{t.track&&(t.track.enabled=e)}))}enableSenderTracks(e){const t=this.peerConnection;if(!t)throw new Error("Peer connection closed.");t.getSenders().forEach((t=>{t.track&&(t.track.enabled=e)}))}getDescription(e,t){var s,i;if(this.logger.debug("SessionDescriptionHandler.getDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));this.onDataChannel=null==e?void 0:e.onDataChannel;const r=null===(s=null==e?void 0:e.offerOptions)||void 0===s?void 0:s.iceRestart,n=void 0===(null==e?void 0:e.iceGatheringTimeout)?null===(i=this.sessionDescriptionHandlerConfiguration)||void 0===i?void 0:i.iceGatheringTimeout:null==e?void 0:e.iceGatheringTimeout;return this.getLocalMediaStream(e).then((()=>this.updateDirection(e))).then((()=>this.createDataChannel(e))).then((()=>this.createLocalOfferOrAnswer(e))).then((e=>this.applyModifiers(e,t))).then((e=>this.setLocalSessionDescription(e))).then((()=>this.waitForIceGatheringComplete(r,n))).then((()=>this.getLocalSessionDescription())).then((e=>({body:e.sdp,contentType:"application/sdp"}))).catch((e=>{throw this.logger.error("SessionDescriptionHandler.getDescription failed - "+e),e}))}hasDescription(e){return this.logger.debug("SessionDescriptionHandler.hasDescription"),"application/sdp"===e}iceGatheringComplete(){this.logger.debug("SessionDescriptionHandler.iceGatheringComplete"),void 0!==this.iceGatheringCompleteTimeoutId&&(this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - clearing timeout"),clearTimeout(this.iceGatheringCompleteTimeoutId),this.iceGatheringCompleteTimeoutId=void 0),void 0!==this.iceGatheringCompletePromise&&(this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - resolving promise"),this.iceGatheringCompleteResolve&&this.iceGatheringCompleteResolve(),this.iceGatheringCompletePromise=void 0,this.iceGatheringCompleteResolve=void 0,this.iceGatheringCompleteReject=void 0)}sendDtmf(e,t){if(this.logger.debug("SessionDescriptionHandler.sendDtmf"),void 0===this._peerConnection)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - peer connection closed"),!1;const s=this._peerConnection.getSenders();if(0===s.length)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - no senders"),!1;const i=s[0].dtmf;if(!i)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - no DTMF sender"),!1;const r=null==t?void 0:t.duration,n=null==t?void 0:t.interToneGap;try{i.insertDTMF(e,r,n)}catch(e){return this.logger.error(e.toString()),!1}return this.logger.log("SessionDescriptionHandler.sendDtmf sent via RTP: "+e.toString()),!0}setDescription(e,t,s){if(this.logger.debug("SessionDescriptionHandler.setDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));this.onDataChannel=null==t?void 0:t.onDataChannel;const i="have-local-offer"===this._peerConnection.signalingState?"answer":"offer";return this.getLocalMediaStream(t).then((()=>this.applyModifiers({sdp:e,type:i},s))).then((e=>this.setRemoteSessionDescription(e))).catch((e=>{throw this.logger.error("SessionDescriptionHandler.setDescription failed - "+e),e}))}applyModifiers(e,t){return this.logger.debug("SessionDescriptionHandler.applyModifiers"),t&&0!==t.length?t.reduce(((e,t)=>e.then(t)),Promise.resolve(e)).then((e=>{if(this.logger.debug("SessionDescriptionHandler.applyModifiers - modified sdp"),!e.sdp||!e.type)throw new Error("Invalid SDP.");return{sdp:e.sdp,type:e.type}})):Promise.resolve(e)}createDataChannel(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));if(!0!==(null==e?void 0:e.dataChannel))return Promise.resolve();if(this._dataChannel)return Promise.resolve();switch(this._peerConnection.signalingState){case"stable":this.logger.debug("SessionDescriptionHandler.createDataChannel - creating data channel");try{return this._dataChannel=this._peerConnection.createDataChannel((null==e?void 0:e.dataChannelLabel)||"",null==e?void 0:e.dataChannelOptions),this.onDataChannel&&this.onDataChannel(this._dataChannel),Promise.resolve()}catch(e){return Promise.reject(e)}case"have-remote-offer":return Promise.resolve();default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}}createLocalOfferOrAnswer(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));switch(this._peerConnection.signalingState){case"stable":return this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP offer"),this._peerConnection.createOffer(null==e?void 0:e.offerOptions);case"have-remote-offer":return this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP answer"),this._peerConnection.createAnswer(null==e?void 0:e.answerOptions);default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}}getLocalMediaStream(e){if(this.logger.debug("SessionDescriptionHandler.getLocalMediaStream"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));let t=Object.assign({},null==e?void 0:e.constraints);if(this.localMediaStreamConstraints){if(t.audio=t.audio||this.localMediaStreamConstraints.audio,t.video=t.video||this.localMediaStreamConstraints.video,JSON.stringify(this.localMediaStreamConstraints.audio)===JSON.stringify(t.audio)&&JSON.stringify(this.localMediaStreamConstraints.video)===JSON.stringify(t.video))return Promise.resolve()}else void 0===t.audio&&void 0===t.video&&(t={audio:!0});return this.localMediaStreamConstraints=t,this.mediaStreamFactory(t,this,e).then((e=>this.setLocalMediaStream(e)))}setLocalMediaStream(e){if(this.logger.debug("SessionDescriptionHandler.setLocalMediaStream"),!this._peerConnection)throw new Error("Peer connection undefined.");const t=this._peerConnection,s=this._localMediaStream,i=[],r=e=>{const r=e.kind;if("audio"!==r&&"video"!==r)throw new Error(`Unknown new track kind ${r}.`);const n=t.getSenders().find((e=>e.track&&e.track.kind===r));n?i.push(new Promise((e=>{this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - replacing sender ${r} track`),e()})).then((()=>n.replaceTrack(e).then((()=>{const t=s.getTracks().find((e=>e.kind===r));t&&(t.stop(),s.removeTrack(t),at.dispatchRemoveTrackEvent(s,t)),s.addTrack(e),at.dispatchAddTrackEvent(s,e)})).catch((e=>{throw this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to replace sender ${r} track`),e}))))):i.push(new Promise((e=>{this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - adding sender ${r} track`),e()})).then((()=>{try{t.addTrack(e,s)}catch(e){throw this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to add sender ${r} track`),e}s.addTrack(e),at.dispatchAddTrackEvent(s,e)})))},n=e.getAudioTracks();n.length&&r(n[0]);const o=e.getVideoTracks();return o.length&&r(o[0]),i.reduce(((e,t)=>e.then((()=>t))),Promise.resolve())}getLocalSessionDescription(){if(this.logger.debug("SessionDescriptionHandler.getLocalSessionDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));const e=this._peerConnection.localDescription;return e?Promise.resolve(e):Promise.reject(new Error("Failed to get local session description"))}setLocalSessionDescription(e){return this.logger.debug("SessionDescriptionHandler.setLocalSessionDescription"),void 0===this._peerConnection?Promise.reject(new Error("Peer connection closed.")):this._peerConnection.setLocalDescription(e)}setRemoteSessionDescription(e){if(this.logger.debug("SessionDescriptionHandler.setRemoteSessionDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));const t=e.sdp;let s;switch(this._peerConnection.signalingState){case"stable":s="offer";break;case"have-local-offer":s="answer";break;default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}return t?this._peerConnection.setRemoteDescription({sdp:t,type:s}):(this.logger.error("SessionDescriptionHandler.setRemoteSessionDescription failed - cannot set null sdp"),Promise.reject(new Error("SDP is undefined")))}setRemoteTrack(e){this.logger.debug("SessionDescriptionHandler.setRemoteTrack");const t=this._remoteMediaStream;t.getTrackById(e.id)?this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - have remote ${e.kind} track`):"audio"===e.kind?(this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${e.kind} track`),t.getAudioTracks().forEach((e=>{e.stop(),t.removeTrack(e),at.dispatchRemoveTrackEvent(t,e)})),t.addTrack(e),at.dispatchAddTrackEvent(t,e)):"video"===e.kind&&(this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${e.kind} track`),t.getVideoTracks().forEach((e=>{e.stop(),t.removeTrack(e),at.dispatchRemoveTrackEvent(t,e)})),t.addTrack(e),at.dispatchAddTrackEvent(t,e))}updateDirection(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));switch(this._peerConnection.signalingState){case"stable":this.logger.debug("SessionDescriptionHandler.updateDirection - setting offer direction");{const t=t=>{switch(t){case"inactive":case"recvonly":return(null==e?void 0:e.hold)?"inactive":"recvonly";case"sendonly":case"sendrecv":return(null==e?void 0:e.hold)?"sendonly":"sendrecv";case"stopped":return"stopped";default:throw new Error("Should never happen")}};this._peerConnection.getTransceivers().forEach((e=>{if(e.direction){const s=t(e.direction);e.direction!==s&&(e.direction=s)}}))}break;case"have-remote-offer":this.logger.debug("SessionDescriptionHandler.updateDirection - setting answer direction");{const t=(()=>{const e=this._peerConnection.remoteDescription;if(!e)throw new Error("Failed to read remote offer");const t=/a=sendrecv\r\n|a=sendonly\r\n|a=recvonly\r\n|a=inactive\r\n/.exec(e.sdp);if(t)switch(t[0]){case"a=inactive\r\n":return"inactive";case"a=recvonly\r\n":return"recvonly";case"a=sendonly\r\n":return"sendonly";case"a=sendrecv\r\n":return"sendrecv";default:throw new Error("Should never happen")}return"sendrecv"})(),s=(()=>{switch(t){case"inactive":return"inactive";case"recvonly":return"sendonly";case"sendonly":return(null==e?void 0:e.hold)?"inactive":"recvonly";case"sendrecv":return(null==e?void 0:e.hold)?"sendonly":"sendrecv";default:throw new Error("Should never happen")}})();this._peerConnection.getTransceivers().forEach((e=>{e.direction&&"stopped"!==e.direction&&e.direction!==s&&(e.direction=s)}))}break;default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}return Promise.resolve()}waitForIceGatheringComplete(e=!1,t=0){return this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete"),void 0===this._peerConnection?Promise.reject("Peer connection closed."):e||"complete"!==this._peerConnection.iceGatheringState?(void 0!==this.iceGatheringCompletePromise&&(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - rejecting prior waiting promise"),this.iceGatheringCompleteReject&&this.iceGatheringCompleteReject(new Error("Promise superseded.")),this.iceGatheringCompletePromise=void 0,this.iceGatheringCompleteResolve=void 0,this.iceGatheringCompleteReject=void 0),this.iceGatheringCompletePromise=new Promise(((e,s)=>{this.iceGatheringCompleteResolve=e,this.iceGatheringCompleteReject=s,t>0&&(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout in "+t),this.iceGatheringCompleteTimeoutId=setTimeout((()=>{this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout"),this.iceGatheringComplete()}),t))})),this.iceGatheringCompletePromise):(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - already complete"),Promise.resolve())}initPeerConnectionEventHandlers(){if(this.logger.debug("SessionDescriptionHandler.initPeerConnectionEventHandlers"),!this._peerConnection)throw new Error("Peer connection undefined.");const e=this._peerConnection;e.onconnectionstatechange=t=>{var s;const i=e.connectionState;this.logger.debug(`SessionDescriptionHandler.onconnectionstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onconnectionstatechange)&&this._peerConnectionDelegate.onconnectionstatechange(t)},e.ondatachannel=e=>{var t;this.logger.debug("SessionDescriptionHandler.ondatachannel"),this._dataChannel=e.channel,this.onDataChannel&&this.onDataChannel(this._dataChannel),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.ondatachannel)&&this._peerConnectionDelegate.ondatachannel(e)},e.onicecandidate=e=>{var t;this.logger.debug("SessionDescriptionHandler.onicecandidate"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onicecandidate)&&this._peerConnectionDelegate.onicecandidate(e)},e.onicecandidateerror=e=>{var t;this.logger.debug("SessionDescriptionHandler.onicecandidateerror"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onicecandidateerror)&&this._peerConnectionDelegate.onicecandidateerror(e)},e.oniceconnectionstatechange=t=>{var s;const i=e.iceConnectionState;this.logger.debug(`SessionDescriptionHandler.oniceconnectionstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.oniceconnectionstatechange)&&this._peerConnectionDelegate.oniceconnectionstatechange(t)},e.onicegatheringstatechange=t=>{var s;const i=e.iceGatheringState;this.logger.debug(`SessionDescriptionHandler.onicegatheringstatechange ${i}`),"complete"===i&&this.iceGatheringComplete(),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onicegatheringstatechange)&&this._peerConnectionDelegate.onicegatheringstatechange(t)},e.onnegotiationneeded=e=>{var t;this.logger.debug("SessionDescriptionHandler.onnegotiationneeded"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onnegotiationneeded)&&this._peerConnectionDelegate.onnegotiationneeded(e)},e.onsignalingstatechange=t=>{var s;const i=e.signalingState;this.logger.debug(`SessionDescriptionHandler.onsignalingstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onsignalingstatechange)&&this._peerConnectionDelegate.onsignalingstatechange(t)},e.ontrack=e=>{var t;const s=e.track.kind,i=e.track.enabled?"enabled":"disabled";this.logger.debug(`SessionDescriptionHandler.ontrack ${s} ${i}`),this.setRemoteTrack(e.track),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.ontrack)&&this._peerConnectionDelegate.ontrack(e)}}}function ct(e){return(t,s)=>{void 0===e&&(e=nt());const i={iceGatheringTimeout:void 0!==(null==s?void 0:s.iceGatheringTimeout)?null==s?void 0:s.iceGatheringTimeout:5e3,peerConnectionConfiguration:Object.assign(Object.assign({},{bundlePolicy:"balanced",certificates:void 0,iceCandidatePoolSize:0,iceServers:[{urls:"stun:stun.l.google.com:19302"}],iceTransportPolicy:"all",rtcpMuxPolicy:"require"}),null==s?void 0:s.peerConnectionConfiguration)},r=t.userAgent.getLogger("sip.SessionDescriptionHandler");return new at(r,e,i)}}class ht{constructor(e,t){if(this._state=oe.Disconnected,this.transitioningState=!1,this._stateEventEmitter=new p,this.logger=e,t){const e=t,s=null==e?void 0:e.wsServers,i=null==e?void 0:e.maxReconnectionAttempts;if(void 0!==s){const e='The transport option "wsServers" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==i){const e='The transport option "maxReconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}s&&!t.server&&("string"==typeof s&&(t.server=s),s instanceof Array&&(t.server=s[0]))}this.configuration=Object.assign(Object.assign({},ht.defaultOptions),t);const s=this.configuration.server,i=y.parse(s,"absoluteURI");if(-1===i)throw this.logger.error(`Invalid WebSocket Server URL "${s}"`),new Error("Invalid WebSocket Server URL");if(!["wss","ws","udp"].includes(i.scheme))throw this.logger.error(`Invalid scheme in WebSocket Server URL "${s}"`),new Error("Invalid scheme in WebSocket Server URL");this._protocol=i.scheme.toUpperCase()}dispose(){return this.disconnect()}get protocol(){return this._protocol}get server(){return this.configuration.server}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get ws(){return this._ws}connect(){return this._connect()}disconnect(){return this._disconnect()}isConnected(){return this.state===oe.Connected}send(e){return this._send(e)}_connect(){switch(this.logger.log(`Connecting ${this.server}`),this.state){case oe.Connecting:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Connecting));if(!this.connectPromise)throw new Error("Connect promise must be defined.");return this.connectPromise;case oe.Connected:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Connecting));if(this.connectPromise)throw new Error("Connect promise must not be defined.");return Promise.resolve();case oe.Disconnecting:if(this.connectPromise)throw new Error("Connect promise must not be defined.");try{this.transitionState(oe.Connecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Disconnected:if(this.connectPromise)throw new Error("Connect promise must not be defined.");try{this.transitionState(oe.Connecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;default:throw new Error("Unknown state")}let e;try{e=new WebSocket(this.server,"sip"),e.binaryType="arraybuffer",e.addEventListener("close",(t=>this.onWebSocketClose(t,e))),e.addEventListener("error",(t=>this.onWebSocketError(t,e))),e.addEventListener("open",(t=>this.onWebSocketOpen(t,e))),e.addEventListener("message",(t=>this.onWebSocketMessage(t,e))),this._ws=e}catch(e){return this._ws=void 0,this.logger.error("WebSocket construction failed."),this.logger.error(e.toString()),new Promise(((t,s)=>{this.connectResolve=t,this.connectReject=s,this.transitionState(oe.Disconnected,e)}))}return this.connectPromise=new Promise(((t,s)=>{this.connectResolve=t,this.connectReject=s,this.connectTimeout=setTimeout((()=>{this.logger.warn("Connect timed out. Exceeded time set in configuration.connectionTimeout: "+this.configuration.connectionTimeout+"s."),e.close(1e3)}),1e3*this.configuration.connectionTimeout)})),this.connectPromise}_disconnect(){switch(this.logger.log(`Disconnecting ${this.server}`),this.state){case oe.Connecting:if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");try{this.transitionState(oe.Disconnecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Connected:if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");try{this.transitionState(oe.Disconnecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Disconnecting:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Disconnecting));if(!this.disconnectPromise)throw new Error("Disconnect promise must be defined.");return this.disconnectPromise;case oe.Disconnected:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Disconnecting));if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");return Promise.resolve();default:throw new Error("Unknown state")}if(!this._ws)throw new Error("WebSocket must be defined.");const e=this._ws;return this.disconnectPromise=new Promise(((t,s)=>{this.disconnectResolve=t,this.disconnectReject=s;try{e.close(1e3)}catch(e){throw this.logger.error("WebSocket close failed."),this.logger.error(e.toString()),e}})),this.disconnectPromise}_send(e){if(!0===this.configuration.traceSip&&this.logger.log("Sending WebSocket message:\n\n"+e+"\n"),this._state!==oe.Connected)return Promise.reject(new Error("Not connected."));if(!this._ws)throw new Error("WebSocket undefined.");try{this._ws.send(e)}catch(e){return e instanceof Error?Promise.reject(e):Promise.reject(new Error("WebSocket send failed."))}return Promise.resolve()}onWebSocketClose(e,t){if(t!==this._ws)return;const s=`WebSocket closed ${this.server} (code: ${e.code})`,i=this.disconnectPromise?void 0:new Error(s);i&&this.logger.warn("WebSocket closed unexpectedly"),this.logger.log(s),this._ws=void 0,this.transitionState(oe.Disconnected,i)}onWebSocketError(e,t){t===this._ws&&this.logger.error("WebSocket error occurred.")}onWebSocketMessage(e,t){if(t!==this._ws)return;const s=e.data;let i;if(/^(\r\n)+$/.test(s))return this.clearKeepAliveTimeout(),void(!0===this.configuration.traceSip&&this.logger.log("Received WebSocket message with CRLF Keep Alive response"));if(s){if("string"!=typeof s){try{i=(new TextDecoder).decode(new Uint8Array(s))}catch(e){return this.logger.error(e.toString()),void this.logger.error("Received WebSocket binary message failed to be converted into string, message discarded")}!0===this.configuration.traceSip&&this.logger.log("Received WebSocket binary message:\n\n"+i+"\n")}else i=s,!0===this.configuration.traceSip&&this.logger.log("Received WebSocket text message:\n\n"+i+"\n");if(this.state===oe.Connected){if(this.onMessage)try{this.onMessage(i)}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onMessage callback"),e}}else this.logger.warn("Received message while not connected, discarding...")}else this.logger.warn("Received empty message, discarding...")}onWebSocketOpen(e,t){t===this._ws&&this._state===oe.Connecting&&(this.logger.log(`WebSocket opened ${this.server}`),this.transitionState(oe.Connected))}transitionLoopDetectedError(e){let t="A state transition loop has been detected.";return t+=` An attempt to transition from ${this._state} to ${e} before the prior transition completed.`,t+=" Perhaps you are synchronously calling connect() or disconnect() from a callback or state change handler?",this.logger.error(t),new d("Loop detected.")}transitionState(e,t){const s=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};if(this.transitioningState)throw this.transitionLoopDetectedError(e);switch(this.transitioningState=!0,this._state){case oe.Connecting:e!==oe.Connected&&e!==oe.Disconnecting&&e!==oe.Disconnected&&s();break;case oe.Connected:e!==oe.Disconnecting&&e!==oe.Disconnected&&s();break;case oe.Disconnecting:e!==oe.Connecting&&e!==oe.Disconnected&&s();break;case oe.Disconnected:e!==oe.Connecting&&s();break;default:throw new Error("Unknown state.")}const i=this._state;this._state=e;const r=this.connectResolve,n=this.connectReject;i===oe.Connecting&&(this.connectPromise=void 0,this.connectResolve=void 0,this.connectReject=void 0);const o=this.disconnectResolve,a=this.disconnectReject;if(i===oe.Disconnecting&&(this.disconnectPromise=void 0,this.disconnectResolve=void 0,this.disconnectReject=void 0),this.connectTimeout&&(clearTimeout(this.connectTimeout),this.connectTimeout=void 0),this.logger.log(`Transitioned from ${i} to ${this._state}`),this._stateEventEmitter.emit(this._state),e===oe.Connected&&(this.startSendingKeepAlives(),this.onConnect))try{this.onConnect()}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onConnect callback"),e}if(i===oe.Connected&&(this.stopSendingKeepAlives(),this.onDisconnect))try{t?this.onDisconnect(t):this.onDisconnect()}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onDisconnect callback"),e}if(i===oe.Connecting){if(!r)throw new Error("Connect resolve undefined.");if(!n)throw new Error("Connect reject undefined.");e===oe.Connected?r():n(t||new Error("Connect aborted."))}if(i===oe.Disconnecting){if(!o)throw new Error("Disconnect resolve undefined.");if(!a)throw new Error("Disconnect reject undefined.");e===oe.Disconnected?o():a(t||new Error("Disconnect aborted."))}this.transitioningState=!1}clearKeepAliveTimeout(){this.keepAliveDebounceTimeout&&clearTimeout(this.keepAliveDebounceTimeout),this.keepAliveDebounceTimeout=void 0}sendKeepAlive(){return this.keepAliveDebounceTimeout?Promise.resolve():(this.keepAliveDebounceTimeout=setTimeout((()=>{this.clearKeepAliveTimeout()}),1e3*this.configuration.keepAliveDebounce),this.send("\r\n\r\n"))}startSendingKeepAlives(){this.configuration.keepAliveInterval&&!this.keepAliveInterval&&(this.keepAliveInterval=setInterval((()=>{this.sendKeepAlive(),this.startSendingKeepAlives()}),(e=>{const t=.8*e;return 1e3*(Math.random()*(e-t)+t)})(this.configuration.keepAliveInterval)))}stopSendingKeepAlives(){this.keepAliveInterval&&clearInterval(this.keepAliveInterval),this.keepAliveDebounceTimeout&&clearTimeout(this.keepAliveDebounceTimeout),this.keepAliveInterval=void 0,this.keepAliveDebounceTimeout=void 0}}ht.defaultOptions={server:"",connectionTimeout:5,keepAliveInterval:0,keepAliveDebounce:10,traceSip:!0};class dt{constructor(e={}){if(this._publishers={},this._registerers={},this._sessions={},this._subscriptions={},this._state=ae.Stopped,this._stateEventEmitter=new p,this.delegate=e.delegate,this.options=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},dt.defaultOptions()),{sipjsId:$(5)}),{uri:new w("sip","anonymous."+$(6),"anonymous.invalid")}),{viaHost:$(12)+".invalid"}),dt.stripUndefinedProperties(e)),this.options.hackIpInContact)if("boolean"==typeof this.options.hackIpInContact&&this.options.hackIpInContact){const e=1,t=254,s=Math.floor(Math.random()*(t-e+1)+e);this.options.viaHost="192.0.2."+s}else this.options.hackIpInContact&&(this.options.viaHost=this.options.hackIpInContact);switch(this.loggerFactory=new Te,this.logger=this.loggerFactory.getLogger("sip.UserAgent"),this.loggerFactory.builtinEnabled=this.options.logBuiltinEnabled,this.loggerFactory.connector=this.options.logConnector,this.options.logLevel){case"error":this.loggerFactory.level=ce.error;break;case"warn":this.loggerFactory.level=ce.warn;break;case"log":this.loggerFactory.level=ce.log;break;case"debug":this.loggerFactory.level=ce.debug}if(this.options.logConfiguration&&(this.logger.log("Configuration:"),Object.keys(this.options).forEach((e=>{const t=this.options[e];switch(e){case"uri":case"sessionDescriptionHandlerFactory":this.logger.log("\xb7 "+e+": "+t);break;case"authorizationPassword":this.logger.log("\xb7 "+e+": NOT SHOWN");break;case"transportConstructor":this.logger.log("\xb7 "+e+": "+t.name);break;default:this.logger.log("\xb7 "+e+": "+JSON.stringify(t))}}))),this.options.transportOptions){const t=this.options.transportOptions,s=t.maxReconnectionAttempts,i=t.reconnectionTimeout;if(void 0!==s){const e='The transport option "maxReconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==i){const e='The transport option "reconnectionTimeout" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}void 0===e.reconnectionDelay&&void 0!==i&&(this.options.reconnectionDelay=i),void 0===e.reconnectionAttempts&&void 0!==s&&(this.options.reconnectionAttempts=s)}if(void 0!==e.reconnectionDelay){const e='The user agent option "reconnectionDelay" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==e.reconnectionAttempts){const e='The user agent option "reconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(this._transport=new this.options.transportConstructor(this.getLogger("sip.Transport"),this.options.transportOptions),this.initTransportCallbacks(),this._contact=this.initContact(),this._instanceId=this.options.instanceId?this.options.instanceId:dt.newUUID(),-1===y.parse(this._instanceId,"uuid"))throw new Error("Invalid instanceId.");this._userAgentCore=this.initCore()}static makeURI(e){return y.URIParse(e)}static defaultOptions(){return{allowLegacyNotifications:!1,authorizationHa1:"",authorizationPassword:"",authorizationUsername:"",delegate:{},contactName:"",contactParams:{transport:"ws"},displayName:"",forceRport:!1,gracefulShutdown:!0,hackAllowUnregisteredOptionTags:!1,hackIpInContact:!1,hackViaTcp:!1,instanceId:"",instanceIdAlwaysAdded:!1,logBuiltinEnabled:!0,logConfiguration:!0,logConnector:()=>{},logLevel:"log",noAnswerTimeout:60,preloadedRouteSet:[],reconnectionAttempts:0,reconnectionDelay:4,sendInitialProvisionalResponse:!0,sessionDescriptionHandlerFactory:ct(),sessionDescriptionHandlerFactoryOptions:{},sipExtension100rel:J.Unsupported,sipExtensionReplaces:J.Unsupported,sipExtensionExtraSupported:[],sipjsId:"",transportConstructor:ht,transportOptions:{},uri:new w("sip","anonymous","anonymous.invalid"),userAgentString:"SIP.js/0.21.2",viaHost:""}}static newUUID(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(e=>{const t=Math.floor(16*Math.random());return("x"===e?t:t%4+8).toString(16)}))}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}get configuration(){return this.options}get contact(){return this._contact}get instanceId(){return this._instanceId}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get transport(){return this._transport}get userAgentCore(){return this._userAgentCore}getLogger(e,t){return this.loggerFactory.getLogger(e,t)}getLoggerFactory(){return this.loggerFactory}isConnected(){return this.transport.isConnected()}reconnect(){return this.state===ae.Stopped?Promise.reject(new Error("User agent stopped.")):Promise.resolve().then((()=>this.transport.connect()))}start(){return this.state===ae.Started?(this.logger.warn("User agent already started"),Promise.resolve()):(this.logger.log(`Starting ${this.configuration.uri}`),this.transitionState(ae.Started),this.transport.connect())}async stop(){if(this.state===ae.Stopped)return this.logger.warn("User agent already stopped"),Promise.resolve();if(this.logger.log(`Stopping ${this.configuration.uri}`),!this.options.gracefulShutdown)return this.logger.log("Dispose of transport"),this.transport.dispose().catch((e=>{throw this.logger.error(e.message),e})),this.logger.log("Dispose of core"),this.userAgentCore.dispose(),this._publishers={},this._registerers={},this._sessions={},this._subscriptions={},this.transitionState(ae.Stopped),Promise.resolve();const e=Object.assign({},this._publishers),t=Object.assign({},this._registerers),s=Object.assign({},this._sessions),i=Object.assign({},this._subscriptions),r=this.transport,n=this.userAgentCore;this.logger.log("Dispose of registerers");for(const e in t)t[e]&&await t[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._registerers[e],t}));this.logger.log("Dispose of sessions");for(const e in s)s[e]&&await s[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._sessions[e],t}));this.logger.log("Dispose of subscriptions");for(const e in i)i[e]&&await i[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._subscriptions[e],t}));this.logger.log("Dispose of publishers");for(const t in e)e[t]&&await e[t].dispose().catch((e=>{throw this.logger.error(e.message),delete this._publishers[t],e}));this.logger.log("Dispose of transport"),await r.dispose().catch((e=>{throw this.logger.error(e.message),e})),this.logger.log("Dispose of core"),n.dispose(),this.transitionState(ae.Stopped)}_makeInviter(e,t){return new ee(this,e,t)}attemptReconnection(e=1){const t=this.options.reconnectionAttempts,s=this.options.reconnectionDelay;e>t?this.logger.log("Maximum reconnection attempts reached"):(this.logger.log(`Reconnection attempt ${e} of ${t} - trying`),setTimeout((()=>{this.reconnect().then((()=>{this.logger.log(`Reconnection attempt ${e} of ${t} - succeeded`)})).catch((s=>{this.logger.error(s.message),this.logger.log(`Reconnection attempt ${e} of ${t} - failed`),this.attemptReconnection(++e)}))}),1===e?0:1e3*s))}initContact(){const e=""!==this.options.contactName?this.options.contactName:$(8),t=this.options.contactParams;return{pubGruu:void 0,tempGruu:void 0,uri:new w("sip",e,this.options.viaHost,void 0,t),toString:(e={})=>{const s=e.anonymous||!1,i=e.outbound||!1,r=e.register||!1;let n="<";return n+=s?this.contact.tempGruu||`sip:anonymous@anonymous.invalid;transport=${t.transport?t.transport:"ws"}`:r?this.contact.uri:this.contact.pubGruu||this.contact.uri,i&&(n+=";ob"),n+=">",this.options.instanceIdAlwaysAdded&&(n+=';+sip.instance=""'),n}}}initCore(){let e=[];e.push("outbound"),this.options.sipExtension100rel===J.Supported&&e.push("100rel"),this.options.sipExtensionReplaces===J.Supported&&e.push("replaces"),this.options.sipExtensionExtraSupported&&e.push(...this.options.sipExtensionExtraSupported),this.options.hackAllowUnregisteredOptionTags||(e=e.filter((e=>X[e]))),e=Array.from(new Set(e));const t=e.slice();(this.contact.pubGruu||this.contact.tempGruu)&&t.push("gruu");const s={aor:this.options.uri,contact:this.contact,displayName:this.options.displayName,loggerFactory:this.loggerFactory,hackViaTcp:this.options.hackViaTcp,routeSet:this.options.preloadedRouteSet,supportedOptionTags:e,supportedOptionTagsResponse:t,sipjsId:this.options.sipjsId,userAgentHeaderFieldValue:this.options.userAgentString,viaForceRport:this.options.forceRport,viaHost:this.options.viaHost,authenticationFactory:()=>{const e=this.options.authorizationUsername?this.options.authorizationUsername:this.options.uri.user,t=this.options.authorizationPassword?this.options.authorizationPassword:void 0,s=this.options.authorizationHa1?this.options.authorizationHa1:void 0;return new we(this.getLoggerFactory(),s,e,t)},transportAccessor:()=>this.transport};return new rt(s,{onInvite:e=>{var t;const s=new Q(this,e);if(e.delegate={onCancel:e=>{s._onCancel(e)},onTransportError:e=>{this.logger.error("A transport error has occurred while handling an incoming INVITE request.")}},e.trying(),this.options.sipExtensionReplaces!==J.Unsupported){const t=e.message.parseHeader("replaces");if(t){const e=t.call_id;if("string"!=typeof e)throw new Error("Type of call id is not string");const i=t.replaces_to_tag;if("string"!=typeof i)throw new Error("Type of to tag is not string");const r=t.replaces_from_tag;if("string"!=typeof r)throw new Error("type of from tag is not string");const n=e+i+r,o=this.userAgentCore.dialogs.get(n);if(!o)return void s.reject({statusCode:481});if(!o.early&&!0===t.early_only)return void s.reject({statusCode:486});const a=this._sessions[e+r]||this._sessions[e+i]||void 0;if(!a)throw new Error("Session does not exist.");s._replacee=a}}if(null===(t=this.delegate)||void 0===t?void 0:t.onInvite)return s.autoSendAnInitialProvisionalResponse?void s.progress().then((()=>{var e;if(void 0===(null===(e=this.delegate)||void 0===e?void 0:e.onInvite))throw new Error("onInvite undefined.");this.delegate.onInvite(s)})):void this.delegate.onInvite(s);s.reject({statusCode:486})},onMessage:e=>{if(this.delegate&&this.delegate.onMessage){const t=new W(e);this.delegate.onMessage(t)}else e.accept()},onNotify:e=>{if(this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else this.options.allowLegacyNotifications?e.accept():e.reject({statusCode:481})},onRefer:e=>{this.logger.warn("Received an out of dialog REFER request"),this.delegate&&this.delegate.onReferRequest?this.delegate.onReferRequest(e):e.reject({statusCode:405})},onRegister:e=>{this.logger.warn("Received an out of dialog REGISTER request"),this.delegate&&this.delegate.onRegisterRequest?this.delegate.onRegisterRequest(e):e.reject({statusCode:405})},onSubscribe:e=>{this.logger.warn("Received an out of dialog SUBSCRIBE request"),this.delegate&&this.delegate.onSubscribeRequest?this.delegate.onSubscribeRequest(e):e.reject({statusCode:405})}})}initTransportCallbacks(){this.transport.onConnect=()=>this.onTransportConnect(),this.transport.onDisconnect=e=>this.onTransportDisconnect(e),this.transport.onMessage=e=>this.onTransportMessage(e)}onTransportConnect(){this.state!==ae.Stopped&&this.delegate&&this.delegate.onConnect&&this.delegate.onConnect()}onTransportDisconnect(e){this.state!==ae.Stopped&&(this.delegate&&this.delegate.onDisconnect&&this.delegate.onDisconnect(e),e&&this.options.reconnectionAttempts>0&&this.attemptReconnection())}onTransportMessage(e){const t=he.parseMessage(e,this.getLogger("sip.Parser"));if(!t)return void this.logger.warn("Failed to parse incoming message. Dropping.");if(this.state===ae.Stopped&&t instanceof P)return void this.logger.warn(`Received ${t.method} request while stopped. Dropping.`);const s=()=>{const e=["from","to","call_id","cseq","via"];for(const s of e)if(!t.hasHeader(s))return this.logger.warn(`Missing mandatory header field : ${s}.`),!1;return!0};if(t instanceof P){if(!s())return void this.logger.warn("Request missing mandatory header field. Dropping.");if(!t.toTag&&t.callId.substr(0,5)===this.options.sipjsId)return void this.userAgentCore.replyStateless(t,{statusCode:482});const e=D(t.body),i=t.getHeader("content-length");if(i&&e1)return void this.logger.warn("More than one Via header field present in the response. Dropping.");if(t.via.host!==this.options.viaHost||void 0!==t.via.port)return void this.logger.warn("Via sent-by in the response does not match UA Via host value. Dropping.");const e=D(t.body),i=t.getHeader("content-length");if(i&&e{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ae.Started:e!==ae.Stopped&&s();break;case ae.Stopped:e!==ae.Started&&s();break;default:throw new Error("Unknown state.")}this.logger.log(`Transitioned from ${this._state} to ${e}`),this._state=e,this._stateEventEmitter.emit(this._state)}}class lt extends He{constructor(e,t,s){super(Ce,e,t,s)}}class gt extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}const ut=(e,t)=>{const s=[],i=e.split(/\r\n/);let r;for(let e=0;e(t.sdp=ut(t.sdp||"",e),Promise.resolve(t))}function bt(e){return e.sdp=((e,t)=>{const s=new RegExp("m="+t+".*$","gm"),i=new RegExp("^a=group:.*$","gm");if(s.test(e)){let s;const r=(e=e.split(/^m=/gm).filter((e=>{if(e.substr(0,t.length)===t){if(s=e.match(/^a=mid:.*$/gm),s){const e=s[0].match(/:.+$/g);e&&(s=e[0].substr(1))}return!1}return!0})).join("m=")).match(i);if(r&&1===r.length){let t=r[0];const n=new RegExp(" *"+s+"[^ ]*","g");t=t.replace(n,""),e=e.split(i).join(t)}}return e})(e.sdp||"","video"),Promise.resolve(e)}function Tt(e){let t=e.sdp||"";if(-1===t.search(/^a=mid.*$/gm)){const s=t.match(/^m=.*$/gm),i=t.split(/^m=.*$/gm);s&&s.forEach(((e,t)=>{s[t]=e+"\na=mid:"+t})),i.forEach(((e,t)=>{s&&s[t]&&(i[t]=e+s[t])})),t=i.join(""),e.sdp=t}return Promise.resolve(e)}function St(e){if(!e.sdp||!e.type)throw new Error("Invalid SDP");let t=e.sdp;const s=e.type;return t&&(/a=(sendrecv|sendonly|recvonly|inactive)/.test(t)?(t=t.replace(/a=sendrecv\r\n/g,"a=sendonly\r\n"),t=t.replace(/a=recvonly\r\n/g,"a=inactive\r\n")):t=t.replace(/(m=[^\r]*\r\n)/g,"$1a=sendonly\r\n")),Promise.resolve({sdp:t,type:s})}function Rt(e){if(e.length<2)throw new Error("Start local conference requires at leaast 2 sessions.");var t;(t=e.map((e=>e.sessionDescriptionHandler)),t.map(((e,s)=>t.slice(s+1).map((t=>[e,t])))).reduce(((e,t)=>e.concat(t)),[])).forEach((([e,t])=>{if(!(e instanceof yt&&t instanceof yt))throw new Error("Session description handler not instance of SessionManagerSessionDescriptionHandler");e.joinWith(t)}))}class yt extends at{constructor(e,t,s){super(e,t,s),yt.audioContext||(yt.audioContext=new AudioContext)}enableSenderTracks(e){const t=this.localMediaStreamReal;if(void 0===t)throw new Error("Stream undefined.");t.getAudioTracks().forEach((t=>{t.enabled=e}))}initLocalMediaStream(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");return this.localMediaStreamReal=e,this.localMediaStreamSourceNode=yt.audioContext.createMediaStreamSource(e),this.localMediaStreamDestinationNode=yt.audioContext.createMediaStreamDestination(),this.localMediaStreamSourceNode.connect(this.localMediaStreamDestinationNode),this.localMediaStreamDestinationNode.stream}joinWith(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");const t=yt.audioContext.createMediaStreamSource(this.remoteMediaStream),s=e.localMediaStreamDestinationNode;if(void 0===s)throw new Error("Peer outbound (local) stream local media stream destination is undefined.");t.connect(s);const i=yt.audioContext.createMediaStreamSource(e.remoteMediaStream),r=this.localMediaStreamDestinationNode;if(void 0===r)throw new Error("Our outbound (local) stream local media stream destination is undefined.");i.connect(r)}setRealLocalMediaStream(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");if(this.localMediaStreamReal){if(!this.localMediaStreamDestinationNode||!this.localMediaStreamSourceNode||!this.localMediaStreamReal)throw new Error("Local media stream undefined.");this.localMediaStreamReal=e,this.localMediaStreamSourceNode.disconnect(this.localMediaStreamDestinationNode),this.localMediaStreamSourceNode=yt.audioContext.createMediaStreamSource(e),this.localMediaStreamSourceNode.connect(this.localMediaStreamDestinationNode)}else this.initLocalMediaStream(e)}}function Et(){return(e,t)=>({session:t,held:!1,muted:!1})}class $t{constructor(e,t={}){this.managedSessions=[],this.attemptingReconnection=!1,this.optionsPingFailure=!1,this.optionsPingRunning=!1,this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.delegate=t.delegate,this.options=Object.assign({aor:"",autoStop:!0,delegate:{},iceStopWaitingOnServerReflexive:!1,managedSessionFactory:(e,t)=>({session:t,held:!1,muted:!1}),maxSimultaneousSessions:2,media:{},optionsPingInterval:-1,optionsPingRequestURI:"",reconnectionAttempts:3,reconnectionDelay:4,registrationRetry:!1,registrationRetryInterval:3,registerGuard:null,registererOptions:{},registererRegisterOptions:{},sendDTMFUsingSessionDescriptionHandler:!1,userAgentOptions:{}},$t.stripUndefinedProperties(t));const s=Object.assign({},t.userAgentOptions);if(s.transportConstructor||(s.transportConstructor=ht),s.transportOptions||(s.transportOptions={server:e}),!s.uri&&t.aor){const e=dt.makeURI(t.aor);if(!e)throw new Error(`Failed to create valid URI from ${t.aor}`);s.uri=e}if(this.userAgent=new dt(s),this.userAgent.delegate={onConnect:()=>{this.logger.log("Connected"),this.delegate&&this.delegate.onServerConnect&&this.delegate.onServerConnect(),this.shouldBeRegistered&&this.register(),this.options.optionsPingInterval>0&&this.optionsPingStart()},onDisconnect:async e=>{this.logger.log("Disconnected");let t=!1;this.options.optionsPingInterval>0&&(t=this.optionsPingFailure,this.optionsPingFailure=!1,this.optionsPingStop()),this.delegate&&this.delegate.onServerDisconnect&&this.delegate.onServerDisconnect(e),(e||t)&&(this.registerer&&(this.logger.log("Disposing of registerer..."),this.registerer.dispose().catch((e=>{this.logger.debug("Error occurred disposing of registerer after connection with server was lost."),this.logger.debug(e.toString())})),this.registerer=void 0),this.managedSessions.slice().map((e=>e.session)).forEach((async e=>{this.logger.log("Disposing of session..."),e.dispose().catch((e=>{this.logger.debug("Error occurred disposing of a session after connection with server was lost."),this.logger.debug(e.toString())}))})),this.shouldBeConnected&&this.attemptReconnection())},onInvite:e=>{this.logger.log(`[${e.id}] Received INVITE`);const t=this.options.maxSimultaneousSessions;if(0!==t&&this.managedSessions.length>t)return this.logger.warn(`[${e.id}] Session already in progress, rejecting INVITE...`),void e.reject().then((()=>{this.logger.log(`[${e.id}] Rejected INVITE`)})).catch((t=>{this.logger.error(`[${e.id}] Failed to reject INVITE`),this.logger.error(t.toString())}));const s={sessionDescriptionHandlerOptions:{constraints:this.constraints}};this.initSession(e,s),this.delegate&&this.delegate.onCallReceived?this.delegate.onCallReceived(e):(this.logger.warn(`[${e.id}] No handler available, rejecting INVITE...`),e.reject().then((()=>{this.logger.log(`[${e.id}] Rejected INVITE`)})).catch((t=>{this.logger.error(`[${e.id}] Failed to reject INVITE`),this.logger.error(t.toString())})))},onMessage:e=>{e.accept().then((()=>{this.delegate&&this.delegate.onMessageReceived&&this.delegate.onMessageReceived(e)}))},onNotify:e=>{e.accept().then((()=>{this.delegate&&this.delegate.onNotificationReceived&&this.delegate.onNotificationReceived(e)}))}},this.registererOptions=Object.assign({},t.registererOptions),this.registererRegisterOptions=Object.assign({},t.registererRegisterOptions),this.options.registrationRetry){this.registererRegisterOptions.requestDelegate=this.registererRegisterOptions.requestDelegate||{};const e=this.registererRegisterOptions.requestDelegate.onReject;this.registererRegisterOptions.requestDelegate.onReject=t=>{e&&e(t),this.attemptRegistration()}}this.logger=this.userAgent.getLogger("sip.SessionManager"),window.addEventListener("online",(()=>{this.logger.log("Online"),this.shouldBeConnected&&this.connect()})),this.options.autoStop&&window.addEventListener("beforeunload",(async()=>{this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.userAgent.state!==ae.Stopped&&await this.userAgent.stop()}))}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}getLocalMediaStream(e){const t=e.sessionDescriptionHandler;if(t){if(!(t instanceof at))throw new Error("Session description handler not instance of web SessionDescriptionHandler");return t.localMediaStream}}getRemoteMediaStream(e){const t=e.sessionDescriptionHandler;if(t){if(!(t instanceof at))throw new Error("Session description handler not instance of web SessionDescriptionHandler");return t.remoteMediaStream}}getLocalAudioTrack(e){var t;return null===(t=this.getLocalMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"audio"===e.kind))}getLocalVideoTrack(e){var t;return null===(t=this.getLocalMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"video"===e.kind))}getRemoteAudioTrack(e){var t;return null===(t=this.getRemoteMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"audio"===e.kind))}getRemoteVideoTrack(e){var t;return null===(t=this.getRemoteMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"video"===e.kind))}async connect(){return this.logger.log("Connecting UserAgent..."),this.shouldBeConnected=!0,this.userAgent.state!==ae.Started?this.userAgent.start():this.userAgent.reconnect()}async disconnect(){return this.logger.log("Disconnecting UserAgent..."),this.userAgent.state===ae.Stopped?Promise.resolve():(this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.registerer=void 0,this.userAgent.stop())}isConnected(){return this.userAgent.isConnected()}async register(e){return this.logger.log("Registering UserAgent..."),this.shouldBeRegistered=!0,void 0!==e&&(this.registererRegisterOptions=Object.assign({},e)),this.registerer||(this.registerer=new ge(this.userAgent,this.registererOptions),this.registerer.stateChange.addListener((e=>{switch(e){case ie.Initial:break;case ie.Registered:this.delegate&&this.delegate.onRegistered&&this.delegate.onRegistered();break;case ie.Unregistered:this.delegate&&this.delegate.onUnregistered&&this.delegate.onUnregistered(),this.shouldBeRegistered&&this.attemptRegistration();break;case ie.Terminated:break;default:throw new Error("Unknown registerer state.")}}))),this.attemptRegistration(!0)}async unregister(e){return this.logger.log("Unregistering UserAgent..."),this.shouldBeRegistered=!1,this.registerer?this.registerer.unregister(e).then((()=>{})):(this.logger.warn("No registerer to unregister."),Promise.resolve())}async call(e,t,s){this.logger.log("Beginning Session...");const i=this.options.maxSimultaneousSessions;if(0!==i&&this.managedSessions.length>i)return Promise.reject(new Error("Maximum number of sessions already exists."));const r=dt.makeURI(e);if(!r)return Promise.reject(new Error(`Failed to create a valid URI from "${e}"`));if(t||(t={}),t.sessionDescriptionHandlerOptions||(t.sessionDescriptionHandlerOptions={}),t.sessionDescriptionHandlerOptions.constraints||(t.sessionDescriptionHandlerOptions.constraints=this.constraints),t.earlyMedia){(s=s||{}).requestDelegate=s.requestDelegate||{};const e=s.requestDelegate.onProgress;s.requestDelegate.onProgress=t=>{183===t.message.statusCode&&this.setupRemoteMedia(n),e&&e(t)}}this.options.iceStopWaitingOnServerReflexive&&(t.delegate=t.delegate||{},t.delegate.onSessionDescriptionHandler=e=>{if(!(e instanceof at))throw new Error("Session description handler not instance of SessionDescriptionHandler");e.peerConnectionDelegate={onicecandidate:t=>{var s;if("srflx"===(null===(s=t.candidate)||void 0===s?void 0:s.type)){this.logger.log(`[${n.id}] Found srflx ICE candidate, stop waiting...`);e.iceGatheringComplete()}}}});const n=new ee(this.userAgent,r,t);return this.sendInvite(n,t,s).then((()=>n))}async hangup(e){return this.logger.log(`[${e.id}] Hangup...`),this.sessionExists(e)?this.terminate(e):Promise.reject(new Error("Session does not exist."))}async answer(e,t){return this.logger.log(`[${e.id}] Accepting Invitation...`),this.sessionExists(e)?e instanceof Q?(t||(t={}),t.sessionDescriptionHandlerOptions||(t.sessionDescriptionHandlerOptions={}),t.sessionDescriptionHandlerOptions.constraints||(t.sessionDescriptionHandlerOptions.constraints=this.constraints),e.accept(t)):Promise.reject(new Error("Session not instance of Invitation.")):Promise.reject(new Error("Session does not exist."))}async decline(e){return this.logger.log(`[${e.id}] Rejecting Invitation...`),this.sessionExists(e)?e instanceof Q?e.reject():Promise.reject(new Error("Session not instance of Invitation.")):Promise.reject(new Error("Session does not exist."))}async hold(e){return this.logger.log(`[${e.id}] Holding session...`),this.setHold(e,!0)}async unhold(e){return this.logger.log(`[${e.id}] Unholding session...`),this.setHold(e,!1)}isHeld(e){const t=this.sessionManaged(e);return!!t&&t.held}mute(e){this.logger.log(`[${e.id}] Disabling media tracks...`),this.setMute(e,!0)}unmute(e){this.logger.log(`[${e.id}] Enabling media tracks...`),this.setMute(e,!1)}isMuted(e){const t=this.sessionManaged(e);return!!t&&t.muted}async sendDTMF(e,t){if(this.logger.log(`[${e.id}] Sending DTMF...`),!/^[0-9A-D#*,]$/.exec(t))return Promise.reject(new Error("Invalid DTMF tone."));if(!this.sessionExists(e))return Promise.reject(new Error("Session does not exist."));if(this.logger.log(`[${e.id}] Sending DTMF tone: ${t}`),this.options.sendDTMFUsingSessionDescriptionHandler)return e.sessionDescriptionHandler?e.sessionDescriptionHandler.sendDtmf(t)?Promise.resolve():Promise.reject(new Error("Failed to send DTMF")):Promise.reject(new Error("Session desciption handler undefined."));{const s={body:{contentDisposition:"render",contentType:"application/dtmf-relay",content:"Signal="+t+"\r\nDuration="+2e3}};return e.info({requestOptions:s}).then((()=>{}))}}async transfer(e,t,s){if(this.logger.log(`[${e.id}] Referring session...`),t instanceof z)return e.refer(t,s).then((()=>{}));const i=dt.makeURI(t);return i?e.refer(i,s).then((()=>{})):Promise.reject(new Error(`Failed to create a valid URI from "${t}"`))}async message(e,t){this.logger.log("Sending message...");const s=dt.makeURI(e);return s?new te(this.userAgent,s,t).message():Promise.reject(new Error(`Failed to create a valid URI from "${e}"`))}get constraints(){let e={audio:!0,video:!1};return this.options.media.constraints&&(e=Object.assign({},this.options.media.constraints)),e}attemptReconnection(e=1){const t=this.options.reconnectionAttempts,s=this.options.reconnectionDelay;this.shouldBeConnected?(this.attemptingReconnection&&this.logger.log("Reconnection attempt already in progress"),e>t?this.logger.log("Reconnection maximum attempts reached"):(1===e?this.logger.log(`Reconnection attempt ${e} of ${t} - trying`):this.logger.log(`Reconnection attempt ${e} of ${t} - trying in ${s} seconds`),this.attemptingReconnection=!0,setTimeout((()=>{if(!this.shouldBeConnected)return this.logger.log(`Reconnection attempt ${e} of ${t} - aborted`),void(this.attemptingReconnection=!1);this.userAgent.reconnect().then((()=>{this.logger.log(`Reconnection attempt ${e} of ${t} - succeeded`),this.attemptingReconnection=!1})).catch((s=>{this.logger.log(`Reconnection attempt ${e} of ${t} - failed`),this.logger.error(s.message),this.attemptingReconnection=!1,this.attemptReconnection(++e)}))}),1===e?0:1e3*s))):this.logger.log("Should not be connected currently")}attemptRegistration(e=!1){if(this.logger.log("Registration attempt "+(e?"without delay":"")),!this.shouldBeRegistered)return this.logger.log("Should not be registered currently"),Promise.resolve();if(void 0!==this.registrationAttemptTimeout)return this.logger.log("Registration attempt already in progress"),Promise.resolve();const t=()=>this.registerer?this.isConnected()?this.userAgent.state===ae.Stopped?(this.logger.log("User agent stopped"),Promise.resolve()):this.options.registerGuard?this.options.registerGuard().catch((e=>{throw this.logger.log("Register guard rejected will making registration attempt"),e})).then((e=>e||!this.registerer?Promise.resolve():this.registerer.register(this.registererRegisterOptions).then((()=>{})))):this.registerer.register(this.registererRegisterOptions).then((()=>{})):(this.logger.log("User agent not connected"),Promise.resolve()):(this.logger.log("Registerer undefined"),Promise.resolve());return new Promise(((s,i)=>{this.registrationAttemptTimeout=setTimeout((()=>{t().then((()=>{this.registrationAttemptTimeout=void 0,s()})).catch((e=>{this.registrationAttemptTimeout=void 0,e instanceof a?s():i(e)}))}),e?0:(e=>{const t=2*e;return 1e3*(Math.random()*(t-e)+e)})(this.options.registrationRetryInterval))}))}cleanupMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");t.mediaLocal&&t.mediaLocal.video&&(t.mediaLocal.video.srcObject=null,t.mediaLocal.video.pause()),t.mediaRemote&&(t.mediaRemote.audio&&(t.mediaRemote.audio.srcObject=null,t.mediaRemote.audio.pause()),t.mediaRemote.video&&(t.mediaRemote.video.srcObject=null,t.mediaRemote.video.pause()))}enableReceiverTracks(e,t){if(!this.sessionExists(e))throw new Error("Session does not exist.");const s=e.sessionDescriptionHandler;if(!(s instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");s.enableReceiverTracks(t)}enableSenderTracks(e,t){if(!this.sessionExists(e))throw new Error("Session does not exist.");const s=e.sessionDescriptionHandler;if(!(s instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");s.enableSenderTracks(t)}initSession(e,t){this.sessionAdd(e),this.delegate&&this.delegate.onCallCreated&&this.delegate.onCallCreated(e),e.stateChange.addListener((t=>{switch(this.logger.log(`[${e.id}] Session state changed to ${t}`),t){case Z.Initial:case Z.Establishing:break;case Z.Established:this.setupLocalMedia(e),this.setupRemoteMedia(e),this.delegate&&this.delegate.onCallAnswered&&this.delegate.onCallAnswered(e);break;case Z.Terminating:case Z.Terminated:this.sessionExists(e)&&(this.cleanupMedia(e),this.sessionRemove(e),this.delegate&&this.delegate.onCallHangup&&this.delegate.onCallHangup(e));break;default:throw new Error("Unknown session state.")}})),e.delegate=e.delegate||{},e.delegate.onInfo=t=>{var s;if(void 0===(null===(s=this.delegate)||void 0===s?void 0:s.onCallDTMFReceived))return void t.reject();const i=t.request.getHeader("content-type");if(!i||!/^application\/dtmf-relay/i.exec(i))return void t.reject();const r=t.request.body.split("\r\n",2);if(2!==r.length)return void t.reject();let n;const o=/^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;if(void 0!==r[0]&&o.test(r[0])&&(n=r[0].replace(o,"$2")),!n)return void t.reject();let a;const c=/^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;void 0!==r[1]&&c.test(r[1])&&(a=parseInt(r[1].replace(c,"$2"),10)),a?t.accept().then((()=>{if(this.delegate&&this.delegate.onCallDTMFReceived){if(!n||!a)throw new Error("Tone or duration undefined.");this.delegate.onCallDTMFReceived(e,n,a)}})).catch((e=>{this.logger.error(e.message)})):t.reject()},e.delegate.onRefer=e=>{e.accept().then((()=>this.sendInvite(e.makeInviter(t),t))).catch((e=>{this.logger.error(e.message)}))}}optionsPingRun(e,t,s){if(this.options.optionsPingInterval<1)throw new Error("Invalid options ping interval.");this.optionsPingRunning||(this.optionsPingRunning=!0,this.optionsPingTimeout=setTimeout((()=>{this.optionsPingTimeout=void 0;const i=()=>{this.optionsPingFailure=!1,this.optionsPingRunning&&(this.optionsPingRunning=!1,this.optionsPingRun(e,t,s))},r=()=>{this.logger.error("OPTIONS ping failed"),this.optionsPingFailure=!0,this.optionsPingRunning=!1,this.userAgent.transport.disconnect().catch((e=>this.logger.error(e)))},n=this.userAgent.userAgentCore,o=n.makeOutgoingRequestMessage("OPTIONS",e,t,s,{});this.optionsPingRequest=n.request(o,{onAccept:()=>{this.optionsPingRequest=void 0,i()},onReject:e=>{this.optionsPingRequest=void 0,408===e.message.statusCode||503===e.message.statusCode?r():i()}})}),1e3*this.options.optionsPingInterval))}optionsPingStart(){let e,t,s;if(this.logger.log("OPTIONS pings started"),this.options.optionsPingRequestURI){if(e=dt.makeURI(this.options.optionsPingRequestURI),!e)throw new Error("Failed to create Request URI.");t=this.userAgent.contact.uri.clone(),s=this.userAgent.contact.uri.clone()}else{if(!this.options.aor)return void this.logger.error("You have enabled sending OPTIONS pings and as such you must provide either a) an AOR to register, or b) an RURI to use for the target of the OPTIONS ping requests. ");{const i=dt.makeURI(this.options.aor);if(!i)throw new Error("Failed to create URI.");e=i.clone(),e.user=void 0,t=i.clone(),s=i.clone()}}this.optionsPingRun(e,t,s)}optionsPingStop(){this.logger.log("OPTIONS pings stopped"),this.optionsPingRunning=!1,this.optionsPingFailure=!1,this.optionsPingRequest&&(this.optionsPingRequest.dispose(),this.optionsPingRequest=void 0),this.optionsPingTimeout&&(clearTimeout(this.optionsPingTimeout),this.optionsPingTimeout=void 0)}async sendInvite(e,t,s){return this.initSession(e,t),e.invite(s).then((()=>{this.logger.log(`[${e.id}] Sent INVITE`)}))}sessionAdd(e){const t=this.options.managedSessionFactory(this,e);this.managedSessions.push(t)}sessionExists(e){return void 0!==this.sessionManaged(e)}sessionManaged(e){return this.managedSessions.find((t=>t.session.id===e.id))}sessionRemove(e){this.managedSessions=this.managedSessions.filter((t=>t.session.id!==e.id))}async setHold(e,t){if(!this.sessionExists(e))return Promise.reject(new Error("Session does not exist."));if(this.isHeld(e)===t)return Promise.resolve();if(!(e.sessionDescriptionHandler instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");const s={requestDelegate:{onAccept:()=>{const s=this.sessionManaged(e);void 0!==s&&(s.held=t,this.enableReceiverTracks(e,!s.held),this.enableSenderTracks(e,!s.held&&!s.muted),this.delegate&&this.delegate.onCallHold&&this.delegate.onCallHold(e,s.held))},onReject:()=>{this.logger.warn(`[${e.id}] Re-invite request was rejected`);const s=this.sessionManaged(e);void 0!==s&&(s.held=!t,this.enableReceiverTracks(e,!s.held),this.enableSenderTracks(e,!s.held&&!s.muted),this.delegate&&this.delegate.onCallHold&&this.delegate.onCallHold(e,s.held))}}},i=e.sessionDescriptionHandlerOptionsReInvite;i.hold=t,e.sessionDescriptionHandlerOptionsReInvite=i;const r=this.sessionManaged(e);if(!r)throw new Error("Managed session is undefiend.");return r.held=t,e.invite(s).then((()=>{const t=this.sessionManaged(e);void 0!==t&&(this.enableReceiverTracks(e,!t.held),this.enableSenderTracks(e,!t.held&&!t.muted))})).catch((s=>{throw r.held=!t,s instanceof a&&this.logger.error(`[${e.id}] A hold request is already in progress.`),s}))}setMute(e,t){if(!this.sessionExists(e))return void this.logger.warn(`[${e.id}] A session is required to enabled/disable media tracks`);if(e.state!==Z.Established)return void this.logger.warn(`[${e.id}] An established session is required to enable/disable media tracks`);const s=this.sessionManaged(e);void 0!==s&&(s.muted=t,this.enableSenderTracks(e,!s.held&&!s.muted))}setupLocalMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");const s="function"==typeof this.options.media.local?this.options.media.local(e):this.options.media.local;t.mediaLocal=s;const i=null==s?void 0:s.video;if(i){const t=this.getLocalMediaStream(e);if(!t)throw new Error("Local media stream undefiend.");i.srcObject=t,i.volume=0,i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play local media`),this.logger.error(t.message)}))}}setupRemoteMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");const s="function"==typeof this.options.media.remote?this.options.media.remote(e):this.options.media.remote;t.mediaRemote=s;const i=(null==s?void 0:s.video)||(null==s?void 0:s.audio);if(i){const t=this.getRemoteMediaStream(e);if(!t)throw new Error("Remote media stream undefiend.");i.autoplay=!0,i.srcObject=t,i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play remote media`),this.logger.error(t.message)})),t.onaddtrack=()=>{this.logger.log("Remote media onaddtrack"),i.load(),i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play remote media`),this.logger.error(t.message)}))}}}async terminate(e){switch(this.logger.log(`[${e.id}] Terminating...`),e.state){case Z.Initial:if(e instanceof ee)return e.cancel().then((()=>{this.logger.log(`[${e.id}] Inviter never sent INVITE (canceled)`)}));if(e instanceof Q)return e.reject().then((()=>{this.logger.log(`[${e.id}] Invitation rejected (sent 480)`)}));throw new Error("Unknown session type.");case Z.Establishing:if(e instanceof ee)return e.cancel().then((()=>{this.logger.log(`[${e.id}] Inviter canceled (sent CANCEL)`)}));if(e instanceof Q)return e.reject().then((()=>{this.logger.log(`[${e.id}] Invitation rejected (sent 480)`)}));throw new Error("Unknown session type.");case Z.Established:return e.bye().then((()=>{this.logger.log(`[${e.id}] Session ended (sent BYE)`)}));case Z.Terminating:case Z.Terminated:break;default:throw new Error("Unknown state")}return this.logger.log(`[${e.id}] Terminating in state ${e.state}, no action taken`),Promise.resolve()}}class It{constructor(e,t={}){this.session=void 0,this.delegate=t.delegate,this.options=Object.assign({},t);const s={aor:this.options.aor,delegate:{onCallAnswered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onCallAnswered)||void 0===t?void 0:t.call(e)},onCallCreated:e=>{var t,s;this.session=e,null===(s=null===(t=this.delegate)||void 0===t?void 0:t.onCallCreated)||void 0===s||s.call(t)},onCallReceived:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onCallReceived)||void 0===t?void 0:t.call(e)},onCallHangup:()=>{var e,t;this.session=void 0,(null===(e=this.delegate)||void 0===e?void 0:e.onCallHangup)&&(null===(t=this.delegate)||void 0===t||t.onCallHangup())},onCallHold:(e,t)=>{var s,i;return null===(i=null===(s=this.delegate)||void 0===s?void 0:s.onCallHold)||void 0===i?void 0:i.call(s,t)},onCallDTMFReceived:(e,t,s)=>{var i,r;return null===(r=null===(i=this.delegate)||void 0===i?void 0:i.onCallDTMFReceived)||void 0===r?void 0:r.call(i,t,s)},onMessageReceived:e=>{var t,s;return null===(s=null===(t=this.delegate)||void 0===t?void 0:t.onMessageReceived)||void 0===s?void 0:s.call(t,e.request.body)},onRegistered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onRegistered)||void 0===t?void 0:t.call(e)},onUnregistered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onUnregistered)||void 0===t?void 0:t.call(e)},onServerConnect:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onServerConnect)||void 0===t?void 0:t.call(e)},onServerDisconnect:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onServerDisconnect)||void 0===t?void 0:t.call(e)}},maxSimultaneousSessions:1,media:this.options.media,reconnectionAttempts:this.options.reconnectionAttempts,reconnectionDelay:this.options.reconnectionDelay,registererOptions:this.options.registererOptions,sendDTMFUsingSessionDescriptionHandler:this.options.sendDTMFUsingSessionDescriptionHandler,userAgentOptions:this.options.userAgentOptions};this.sessionManager=new $t(e,s),this.logger=this.sessionManager.userAgent.getLogger("sip.SimpleUser")}get id(){return this.options.userAgentOptions&&this.options.userAgentOptions.displayName||"Anonymous"}get localMediaStream(){return this.session&&this.sessionManager.getLocalMediaStream(this.session)}get remoteMediaStream(){return this.session&&this.sessionManager.getRemoteMediaStream(this.session)}get localAudioTrack(){return this.session&&this.sessionManager.getLocalAudioTrack(this.session)}get localVideoTrack(){return this.session&&this.sessionManager.getLocalVideoTrack(this.session)}get remoteAudioTrack(){return this.session&&this.sessionManager.getRemoteAudioTrack(this.session)}get remoteVideoTrack(){return this.session&&this.sessionManager.getRemoteVideoTrack(this.session)}connect(){return this.logger.log(`[${this.id}] Connecting UserAgent...`),this.sessionManager.connect()}disconnect(){return this.logger.log(`[${this.id}] Disconnecting UserAgent...`),this.sessionManager.disconnect()}isConnected(){return this.sessionManager.isConnected()}register(e){return this.logger.log(`[${this.id}] Registering UserAgent...`),this.sessionManager.register(e)}unregister(e){return this.logger.log(`[${this.id}] Unregistering UserAgent...`),this.sessionManager.unregister(e)}call(e,t,s){return this.logger.log(`[${this.id}] Beginning Session...`),this.session?Promise.reject(new Error("Session already exists.")):this.sessionManager.call(e,t,s).then((()=>{}))}hangup(){return this.logger.log(`[${this.id}] Hangup...`),this.session?this.sessionManager.hangup(this.session).then((()=>{this.session=void 0})):Promise.reject(new Error("Session does not exist."))}answer(e){return this.logger.log(`[${this.id}] Accepting Invitation...`),this.session?this.sessionManager.answer(this.session,e):Promise.reject(new Error("Session does not exist."))}decline(){return this.logger.log(`[${this.id}] rejecting Invitation...`),this.session?this.sessionManager.decline(this.session):Promise.reject(new Error("Session does not exist."))}hold(){return this.logger.log(`[${this.id}] holding session...`),this.session?this.sessionManager.hold(this.session):Promise.reject(new Error("Session does not exist."))}unhold(){return this.logger.log(`[${this.id}] unholding session...`),this.session?this.sessionManager.unhold(this.session):Promise.reject(new Error("Session does not exist."))}isHeld(){return!!this.session&&this.sessionManager.isHeld(this.session)}mute(){return this.logger.log(`[${this.id}] disabling media tracks...`),this.session&&this.sessionManager.mute(this.session)}unmute(){return this.logger.log(`[${this.id}] enabling media tracks...`),this.session&&this.sessionManager.unmute(this.session)}isMuted(){return!!this.session&&this.sessionManager.isMuted(this.session)}sendDTMF(e){return this.logger.log(`[${this.id}] sending DTMF...`),this.session?this.sessionManager.sendDTMF(this.session,e):Promise.reject(new Error("Session does not exist."))}message(e,t){return this.logger.log(`[${this.id}] sending message...`),this.sessionManager.message(e,t)}}const Ct=r,At="sip.js";return t})())); \ No newline at end of file diff --git a/ui/operator/styles.css b/ui/operator/styles.css new file mode 100644 index 0000000..c7c83e7 --- /dev/null +++ b/ui/operator/styles.css @@ -0,0 +1,5989 @@ +:root { + --bg-app: #f5f7fb; + --bg-soft: #eef2f8; + --bg-panel: #ffffff; + --bg-panel-2: #fbfcff; + --line: #d9e0ea; + --line-strong: #cad4e2; + --text: #10213d; + --text-soft: #617089; + --text-faint: #8c99ad; + --primary: #3366e8; + --primary-soft: #edf2ff; + --primary-line: #d4def9; + --success: #0f9f6f; + --success-soft: #e8f9f1; + --warning: #f59e0b; + --warning-soft: #fff5dd; + --danger: #ef4444; + --danger-soft: #fff0f0; + --shadow: 0 18px 40px rgba(16, 33, 61, 0.06); + --radius-xl: 22px; + --radius-lg: 16px; + --radius-md: 12px; +} + +* { + box-sizing: border-box; +} + +[hidden] { + display: none !important; +} + +html, +body { + margin: 0; + min-height: 100vh; +} + +body { + background: + radial-gradient(circle at top right, rgba(51, 102, 232, 0.06), transparent 28%), + linear-gradient(180deg, #f8faff 0%, var(--bg-app) 32%, #f2f5fa 100%); + color: var(--text); + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; +} + +h1, +h2, +.eyebrow, +.btn, +.brand-title, +.nav-link, +.summary-value, +.status-pill { + font-family: "Manrope", "IBM Plex Sans", sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +.bg-orb { + display: none; +} + +.app-shell { + min-height: 100vh; + display: block; +} + +.sidebar { + width: 300px; + background: rgba(255, 255, 255, 0.9); + border-right: 1px solid var(--line); + padding: 24px 20px; + display: flex; + flex-direction: column; + gap: 24px; + position: fixed; + top: 0; + left: 0; + bottom: 0; + min-height: 100vh; + overflow-y: auto; + z-index: 20; +} + +.brand { + display: flex; + align-items: center; + gap: 14px; + padding: 4px 8px; +} + +.brand-mark { + width: 40px; + height: 40px; + border-radius: 12px; + display: grid; + place-items: center; + background: linear-gradient(135deg, #2d63ea, #4b79f5); + color: #fff; + font-size: 22px; + font-weight: 800; +} + +.brand-title { + font-size: 28px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.brand-sub { + margin-top: 4px; + color: var(--text-soft); + font-size: 13px; +} + +.nav-menu { + display: grid; + gap: 8px; +} + +.nav-group { + display: grid; + gap: 6px; +} + +.nav-label { + padding: 0 12px; + font-size: 11px; + font-weight: 700; + color: var(--text-faint); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.nav-link { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: 14px; + color: var(--text-soft); + font-size: 15px; + font-weight: 700; + transition: 0.18s ease; +} + +.nav-link:hover { + background: #f4f7fd; + color: var(--text); +} + +.nav-link.active { + background: var(--primary-soft); + color: var(--primary); + border: 1px solid var(--primary-line); +} + +.nav-bullet { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + opacity: 0.25; +} + +.nav-link.active .nav-bullet { + opacity: 1; +} + +.sidebar-footer { + margin-top: auto; + padding: 16px 14px; + border-radius: 16px; + background: linear-gradient(180deg, #f7f9fe, #eef3fb); + border: 1px solid var(--line); +} + +.sidebar-footer strong { + display: block; + margin-bottom: 6px; +} + +.sidebar-footer p { + margin: 0; + font-size: 13px; + color: var(--text-soft); + line-height: 1.45; +} + +.sidebar-footer .btn { + width: 100%; + margin-top: 12px; +} + +.login-shell { + min-height: 100vh; + display: grid; + place-items: center; + padding: 32px; +} + +.login-layout { + width: min(1120px, 100%); +} + +.login-card { + padding: 26px; +} + +.login-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + align-items: end; + margin-top: 18px; +} + +.login-layout .login-grid > .row:nth-child(2) { + display: none; +} + +.recording-detail { + display: grid; + gap: 12px; + margin-top: 16px; +} + +.recording-detail audio { + width: 100%; +} + +.login-grid .actions { + flex-direction: column; +} + +.login-brand { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 18px; +} + +.login-brand-copy strong { + display: block; + font-size: 26px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.login-brand-copy span { + color: var(--text-soft); + font-size: 14px; +} + +.workspace { + min-width: 0; + margin-left: 300px; +} + +.workspace-topbar { + height: 84px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 16px 32px; + background: rgba(255, 255, 255, 0.88); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 10; +} + +.search-box { + flex: 1; + max-width: 720px; + position: relative; +} + +.search-box::before { + content: "⌕"; + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-faint); + font-size: 18px; + pointer-events: none; +} + +.app-search { + width: 100%; + height: 48px; + padding: 0 16px 0 48px; + border-radius: 14px; + border: 1px solid var(--line); + background: #f8faff; + color: var(--text); + font-size: 16px; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 14px; +} + +.phone-entrypoint { + position: relative; +} + +.phone-chip { + min-height: 42px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid var(--line); + background: #fff; + color: var(--text); + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; +} + +.phone-chip-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #a9b5c7; + flex: 0 0 auto; +} + +.phone-chip-copy { + display: grid; + gap: 1px; + text-align: left; +} + +.phone-chip-title { + font-size: 11px; + font-weight: 800; + color: var(--text-faint); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.phone-chip-status { + font-size: 13px; + font-weight: 800; +} + +.phone-chip[data-state="registered"] .phone-chip-dot, +.phone-chip[data-state="active"] .phone-chip-dot { + background: var(--success); +} + +.phone-chip[data-state="incoming"] .phone-chip-dot { + background: var(--warning); +} + +.phone-chip[data-state="connecting"] .phone-chip-dot, +.phone-chip[data-state="ending"] .phone-chip-dot { + background: var(--primary); +} + +.phone-chip[data-state="disabled"] .phone-chip-dot, +.phone-chip[data-state="unavailable"] .phone-chip-dot, +.phone-chip[data-state="offline"] .phone-chip-dot { + background: var(--text-faint); +} + +.phone-settings { + position: absolute; + top: calc(100% + 10px); + right: 0; + width: min(420px, 92vw); + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.98); + box-shadow: 0 20px 40px rgba(16, 33, 61, 0.14); + z-index: 30; +} + +.icon-button { + width: 42px; + height: 42px; + border-radius: 50%; + border: 1px solid var(--line); + background: #fff; + color: var(--text-soft); + display: grid; + place-items: center; + font-size: 18px; + cursor: pointer; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 8px; + height: 42px; + padding: 0 14px; + border-radius: 999px; + border: 1px solid #cfe8d6; + background: #f2fbf5; + color: #157347; + font-size: 14px; + font-weight: 800; +} + +.status-pill::before { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: currentColor; +} + +.profile-chip { + display: flex; + align-items: center; + gap: 12px; + padding-left: 10px; + border-left: 1px solid var(--line); +} + +.avatar-badge { + width: 42px; + height: 42px; + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(180deg, #eef3ff, #e2ebff); + color: var(--primary); + font-weight: 800; +} + +.profile-meta { + min-width: 0; +} + +.profile-name { + font-weight: 700; +} + +.profile-role { + color: var(--text-soft); + font-size: 12px; +} + +.layout { + padding: 28px 32px 40px; +} + +.operator-view { + display: grid; + gap: 18px; +} + +.page-hero { + margin-bottom: 18px; +} + +.customers-page-hero { + margin-bottom: 4px; +} + +.customers-page-hero .eyebrow { + display: none; +} + +.customers-page-hero h1 { + margin: 0 0 8px; + font-size: clamp(32px, 3.5vw, 44px); + line-height: 1.05; +} + +.customers-page-hero .hint { + font-size: 16px; + color: #6c7890; +} + +.eyebrow { + margin: 0; + color: var(--primary); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +h1 { + margin: 8px 0 6px; + font-size: clamp(34px, 4vw, 52px); + line-height: 1.04; + letter-spacing: -0.04em; +} + +.page-hero .hint { + font-size: 18px; + color: var(--text-soft); +} + +.panel { + background: var(--bg-panel); + border: 1px solid var(--line); + border-radius: var(--radius-xl); + box-shadow: var(--shadow); + padding: 20px; +} + +.panel h2 { + margin: 0 0 14px; + font-size: 22px; + line-height: 1.15; + letter-spacing: -0.02em; +} + +.auth { + margin-bottom: 18px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + align-items: end; +} + +.auth h2, +.auth .hint { + grid-column: 1 / -1; +} + +.row { + display: grid; + gap: 6px; +} + +label { + font-size: 13px; + font-weight: 700; + color: var(--text-soft); +} + +input, +select, +textarea { + width: 100%; + min-height: 46px; + border-radius: 14px; + border: 1px solid var(--line); + background: var(--bg-panel-2); + color: var(--text); + font: inherit; + padding: 0 14px; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease; +} + +input:focus, +select:focus, +textarea:focus { + border-color: #b8c8ef; + background: #fff; + box-shadow: 0 0 0 4px rgba(51, 102, 232, 0.08); +} + +input::placeholder, +textarea::placeholder { + color: var(--text-faint); +} + +.actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn, +.row-actions button { + min-height: 44px; + padding: 0 16px; + border: 1px solid transparent; + border-radius: 14px; + background: linear-gradient(180deg, #3f71ef, #2f61e0); + color: #fff; + font-size: 14px; + font-weight: 800; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; + box-shadow: 0 10px 18px rgba(51, 102, 232, 0.18); +} + +.btn:hover, +.row-actions button:hover { + transform: translateY(-1px); +} + +.btn.ghost, +.row-actions button { + background: #fff; + color: var(--primary); + border-color: var(--primary-line); + box-shadow: none; +} + +.btn.danger { + background: linear-gradient(180deg, #fa5d5d, #e33d3d); + color: #fff; + border-color: transparent; +} + +.hint { + margin: 0; + color: var(--text-soft); + font-size: 13px; + line-height: 1.45; +} + +.hint strong { + color: var(--text); +} + +.demo-guide { + margin-bottom: 18px; + background: linear-gradient(180deg, #ffffff, #f9fbff); +} + +.guide-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.guide-badge { + padding: 9px 12px; + border-radius: 999px; + border: 1px solid var(--primary-line); + background: var(--primary-soft); + color: var(--primary); + font-size: 12px; + font-weight: 800; +} + +.guide-list { + margin: 14px 0 0; + padding-left: 20px; + display: grid; + gap: 8px; + color: var(--text-soft); + font-size: 14px; +} + +.grid { + display: grid; + grid-template-columns: repeat(2, minmax(320px, 1fr)); + gap: 18px; +} + +.panel-span-full { + grid-column: 1 / -1; +} + +.inline-form { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 10px; +} + +.inline-form.compact { + grid-template-columns: 1fr auto; +} + +.inline-form.split-two { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.checkbox-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.92rem; + color: var(--text-muted); +} + +.checkbox-label input { + width: auto; + min-height: auto; + margin: 0; +} + +.json-textarea { + min-height: 220px; + font-family: "IBM Plex Mono", "IBM Plex Sans", monospace; + font-size: 0.9rem; +} + +.list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 10px; + max-height: 280px; + overflow: auto; +} + +.list li { + padding: 14px; + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: linear-gradient(180deg, #fff, #fbfcff); + box-shadow: 0 8px 20px rgba(16, 33, 61, 0.04); + font-size: 14px; + line-height: 1.45; +} + +.customers-panel { + display: grid; + gap: 18px; + padding: 0; + overflow: hidden; +} + +.customers-toolbar, +.customers-toolbar-left, +.customers-toolbar-right, +.customers-footer, +.customers-pager { + display: flex; + align-items: center; + gap: 12px; +} + +.customers-toolbar, +.customers-footer { + justify-content: space-between; + flex-wrap: wrap; + padding: 18px 18px 0; +} + +.customers-footer { + padding: 16px 18px 18px; +} + +.customers-toolbar-left, +.customers-toolbar-right { + flex-wrap: wrap; +} + +.customers-search { + position: relative; + min-width: min(100%, 420px); + max-width: 460px; + flex: 1; +} + +.customers-search input { + width: 100%; + height: 46px; + padding: 0 16px 0 44px; + border-radius: 14px; + border: 1px solid var(--line); + background: #f9fbfd; + color: var(--text); + font-size: 15px; +} + +.customers-search-icon { + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); + color: var(--text-faint); + pointer-events: none; +} + +.customers-filter-btn { + border-radius: 14px; +} + +.customer-lead-form { + margin: 0 18px; + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #f8fbff, #f3f7fd); +} + +.lead-table-shell { + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); + background: #fff; + max-height: 580px; + overflow-y: auto; + overflow-x: hidden; +} + +.lead-table { + display: grid; +} + +.lead-table-head, +.lead-table-row { + display: grid; + grid-template-columns: 52px minmax(260px, 2fr) minmax(140px, 1fr) minmax(150px, 1fr) minmax(170px, 1fr) 110px; + align-items: center; + gap: 12px; +} + +.lead-table-head { + min-height: 50px; + padding: 0 18px; + border-bottom: 1px solid var(--line); + background: #fff; + color: #66748d; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + position: sticky; + top: 0; + z-index: 2; +} + +.lead-table-row { + width: 100%; + min-height: 86px; + padding: 0 18px; + border: 0; + border-bottom: 1px solid var(--line); + background: #fff; + text-align: left; + cursor: pointer; +} + +.lead-table-row:last-child { + border-bottom: 0; +} + +.lead-table-row:hover { + background: #f8fbff; +} + +.lead-table-row.selected { + background: #f4f8ff; +} + +.lead-cell { + min-width: 0; + color: #5f6d86; + font-size: 14px; +} + +.checkbox-cell { + display: inline-flex; + justify-content: center; +} + +.checkbox-cell input { + width: 16px; + height: 16px; + min-height: 16px; + margin: 0; + padding: 0; + border-radius: 4px; + accent-color: var(--primary); + box-shadow: none; +} + +.contact-cell { + display: flex; + align-items: center; + gap: 14px; +} + +.lead-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(180deg, #dfe8ff, #d4dcfa); + color: #3658c9; + font-weight: 800; + font-size: 13px; + letter-spacing: 0.02em; + flex: 0 0 auto; +} + +.lead-contact-copy { + min-width: 0; +} + +.lead-contact-name { + color: var(--text); + font-size: 16px; + font-weight: 800; +} + +.lead-contact-company { + margin-top: 2px; + color: #6f7c93; + font-size: 13px; +} + +.lead-badge { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 0 12px; + border-radius: 999px; + font-size: 13px; + font-weight: 600; +} + +.source-instagram { + color: #9b27d7; + background: #f4e9ff; +} + +.source-whatsapp { + color: #13984a; + background: #e5f8ea; +} + +.source-facebook { + color: #1657d8; + background: #e7efff; +} + +.source-site { + color: #3465f3; + background: #e8f0ff; +} + +.source-referral { + color: #53627a; + background: #eff2f6; +} + +.status-negotiation { + color: #8a2be2; + background: #f2e8ff; +} + +.status-qualification { + color: #2f63f6; + background: #e8f0ff; +} + +.status-proposal { + color: #b56d00; + background: #fff0c7; +} + +.status-initial { + color: #55657a; + background: #eff2f6; +} + +.status-success { + color: #0f8b4c; + background: #dff7e7; +} + +.status-new { + color: #4d57e8; + background: #e8ebff; +} + +.status-lost { + color: #d33c32; + background: #fde8e7; +} + +.score-cell { + text-align: center; +} + +.lead-score { + font-size: 16px; + font-weight: 800; + letter-spacing: 0; +} + +.score-strong { + color: #0d8b57; +} + +.score-medium { + color: #e08a00; +} + +.score-weak { + color: #db4a44; +} + +.lead-empty-state { + padding: 28px; + color: var(--text-soft); + display: grid; + gap: 6px; +} + +.customer-spotlight-shell { + padding: 18px; + border-top: 1px solid var(--line); + background: + radial-gradient(circle at top right, rgba(51, 102, 232, 0.08), transparent 32%), + linear-gradient(180deg, #fcfdff, #f6f9ff); +} + +.customer-spotlight-empty { + padding: 24px; + border-radius: 22px; + border: 1px dashed var(--line-strong); + background: rgba(255, 255, 255, 0.78); + color: var(--text-soft); + display: grid; + gap: 6px; +} + +.customer-spotlight-empty strong { + color: var(--text); + font-size: 18px; +} + +.customer-spotlight-empty p { + margin: 0; + line-height: 1.55; +} + +.customer-spotlight-grid { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.92fr); + gap: 18px; + align-items: start; +} + +.customer-spotlight-grid.page { + grid-template-columns: minmax(0, 1.02fr) minmax(420px, 0.98fr); +} + +.page-hero-actions { + margin-bottom: 12px; +} + +.customer-profile-page-panel { + padding: 0; + overflow: hidden; +} + +.customer-spotlight-page { + border-top: 0; + padding: 22px; +} + +.customer-profile-card, +.customer-history-card { + padding: 20px; + border-radius: 24px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 18px 36px rgba(16, 33, 61, 0.06); + display: grid; + gap: 18px; +} + +.customer-profile-hero, +.customer-history-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} + +.customer-profile-avatar { + width: 64px; + height: 64px; + border-radius: 20px; + flex: 0 0 auto; + display: grid; + place-items: center; + background: linear-gradient(135deg, #3569ec, #7aa2ff); + color: #fff; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 22px; + font-weight: 800; + letter-spacing: 0.04em; + box-shadow: 0 16px 28px rgba(51, 102, 232, 0.2); +} + +.customer-profile-copy { + flex: 1; + min-width: 0; +} + +.customer-profile-name-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-top: 8px; +} + +.customer-profile-name-stack { + min-width: 0; + flex: 1; +} + +.customer-profile-name-trigger { + flex: 0 0 auto; +} + +.customer-profile-name-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} + +.customer-profile-name-note, +.customer-profile-name-flash { + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + display: inline-flex; + align-items: center; + font-size: 12px; + font-weight: 700; +} + +.customer-profile-name-note { + background: #f6f8fc; + border: 1px solid #dce4ef; + color: #52627d; +} + +.customer-profile-name-flash { + background: rgba(16, 185, 129, 0.14); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #117c5f; +} + +.customer-profile-kicker { + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.customer-profile-copy h3, +.customer-history-header h3 { + margin: 8px 0 0; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 30px; + font-weight: 800; + letter-spacing: -0.04em; +} + +.customer-profile-copy p, +.customer-history-caption { + margin: 6px 0 0; + color: var(--text-soft); + line-height: 1.5; +} + +.customer-profile-name-editor { + display: grid; + gap: 10px; + margin-top: 14px; + padding: 14px 16px; + border-radius: 18px; + border: 1px solid rgba(163, 183, 222, 0.55); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 248, 255, 0.96)); +} + +.customer-profile-name-editor-label { + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.customer-profile-name-editor-form { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.customer-profile-name-editor-form input { + min-width: 220px; + flex: 1 1 260px; +} + +.customer-profile-badges { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + max-width: 280px; +} + +.customer-score-pill { + min-height: 26px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + background: #f6f8fc; + border: 1px solid var(--line); + font-size: 13px; + font-weight: 800; +} + +.customer-profile-summary-grid, +.customer-profile-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.customer-meta-card { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #ffffff, #f8fbff); + display: grid; + gap: 8px; +} + +.customer-meta-label { + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.customer-meta-value { + color: var(--text); + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 20px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.customer-meta-note { + color: var(--text-soft); + font-size: 13px; + line-height: 1.45; +} + +.customer-chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.customer-chip, +.customer-channel-pill { + min-height: 30px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + font-size: 13px; + font-weight: 700; +} + +.customer-chip { + background: #edf2ff; + color: #325ed8; +} + +.customer-channel-pill { + background: #eef4f1; + color: #11704e; +} + +.customer-profile-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.customer-history-header { + padding-bottom: 2px; + border-bottom: 1px solid rgba(217, 224, 234, 0.72); +} + +.customer-history-caption { + max-width: 260px; + text-align: right; +} + +.customer-history-list { + display: grid; + gap: 12px; + max-height: 720px; + overflow: auto; + padding-right: 4px; +} + +.customer-history-event { + display: grid; + grid-template-columns: 46px minmax(0, 1fr); + gap: 12px; + align-items: start; +} + +.customer-history-mark { + width: 46px; + height: 46px; + border-radius: 16px; + display: grid; + place-items: center; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + border: 1px solid var(--line); + background: #f5f8fd; + color: var(--text-soft); +} + +.customer-history-body { + padding: 14px 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: #fff; + box-shadow: 0 10px 24px rgba(16, 33, 61, 0.04); + display: grid; + gap: 6px; +} + +.customer-history-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.customer-history-title { + color: var(--text); + font-size: 15px; + font-weight: 800; +} + +.customer-history-time { + color: var(--text-faint); + font-size: 12px; + white-space: nowrap; +} + +.customer-history-copy { + color: var(--text-soft); + font-size: 14px; + line-height: 1.55; +} + +.customer-history-note { + color: var(--text-faint); + font-size: 12px; +} + +.customer-history-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.customer-history-link { + min-height: 36px; + padding: 0 12px; + border-radius: 12px; + font-size: 12px; +} + +.customer-history-event.voice .customer-history-mark { + background: #e8f0ff; + border-color: #cfdcff; + color: #2e63e9; +} + +.customer-history-event.telegram .customer-history-mark { + background: #eaf8ff; + border-color: #c8ebff; + color: #1187c9; +} + +.customer-history-event.email .customer-history-mark { + background: #fff4e5; + border-color: #ffe0b3; + color: #b56d00; +} + +.customer-history-event.webchat .customer-history-mark { + background: #ecfff6; + border-color: #c9f0dd; + color: #0f8b57; +} + +.customer-history-event.profile .customer-history-mark { + background: #f2efff; + border-color: #ddd4ff; + color: #6f4de3; +} + +.customer-history-event.case .customer-history-mark { + background: #f4f7fb; + border-color: #dbe4f1; + color: #4e617f; +} + +.telegram-page-hero { + display: none; +} + +.messenger-panel { + padding: 0; + overflow: hidden; + background: #eef3f8; + height: calc(100dvh - 152px); + min-height: 640px; + display: flex; +} + +.messenger-layout { + width: 100%; + height: 100%; + display: grid; + grid-template-columns: 360px minmax(0, 1fr) 320px; + min-height: 0; +} + +.messenger-list-pane, +.messenger-context-pane { + background: rgba(255, 255, 255, 0.96); + min-width: 0; + min-height: 0; +} + +.messenger-list-pane { + border-right: 1px solid #dde5ef; + display: flex; + flex-direction: column; +} + +.messenger-context-pane { + border-left: 1px solid #dde5ef; + padding: 16px; + overflow: auto; +} + +.messenger-pane-head { + padding: 18px 18px 12px; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.messenger-pane-head h2 { + margin: 3px 0 4px; + font-size: 24px; +} + +.messenger-filters { + padding: 0 14px 12px; + display: flex; + gap: 8px; + overflow-x: auto; +} + +.messenger-filter { + border: 1px solid #dbe5f2; + border-radius: 999px; + background: #f7f9fc; + color: #536179; + cursor: pointer; + font-weight: 800; + min-height: 34px; + padding: 0 12px; + white-space: nowrap; +} + +.messenger-filter.active { + background: #10213d; + border-color: #10213d; + color: #fff; +} + +.messenger-conversation-list { + flex: 1; + overflow: auto; + padding-bottom: 10px; +} + +.messenger-empty-state { + min-height: 180px; + display: grid; + gap: 8px; + place-items: center; + text-align: center; + color: #667085; + padding: 24px; +} + +.messenger-conversation-card { + width: 100%; + border: 0; + border-bottom: 1px solid #edf1f6; + background: transparent; + color: #10213d; + cursor: pointer; + display: flex; + gap: 12px; + padding: 14px 16px; + text-align: left; +} + +.messenger-conversation-card:hover { + background: #f6f9fd; +} + +.messenger-conversation-card.selected { + background: linear-gradient(135deg, #eaf2ff 0%, #f8fbff 100%); +} + +.messenger-conversation-body { + min-width: 0; + flex: 1; +} + +.messenger-conversation-head, +.messenger-conversation-preview-row, +.messenger-conversation-badges { + display: flex; + align-items: center; + gap: 8px; +} + +.messenger-conversation-head, +.messenger-conversation-preview-row { + justify-content: space-between; +} + +.messenger-conversation-name, +.messenger-conversation-preview { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.messenger-conversation-name { + font-weight: 900; +} + +.messenger-conversation-time { + color: #8a95a6; + font-size: 12px; + flex: 0 0 auto; +} + +.messenger-conversation-preview { + color: #667085; + font-size: 13px; +} + +.messenger-conversation-badges { + flex-wrap: wrap; + margin-top: 8px; +} + +.messenger-channel-badge, +.messenger-status-badge { + border-radius: 999px; + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 0 10px; + font-size: 11px; + font-weight: 900; +} + +.messenger-channel-badge { + background: #e6f4fb; + color: #0088cc; +} + +.messenger-channel-badge.compact { + min-height: 21px; + padding: 0 8px; +} + +.messenger-status-badge { + background: #edf2ff; + color: #445274; +} + +.messenger-status-badge.owner { + background: #ecfdf3; + color: #047857; +} + +.messenger-chat-pane { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: + radial-gradient(circle at 12% 12%, rgba(51, 102, 232, 0.08), transparent 24%), + #e7edf5; +} + +.messenger-chat-header { + min-height: 78px; + padding: 14px 18px; + border-bottom: 1px solid rgba(202, 212, 226, 0.8); + background: rgba(255, 255, 255, 0.94); + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; +} + +.messenger-chat-identity, +.messenger-chat-actions { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.messenger-chat-copy { + min-width: 0; +} + +.messenger-chat-title { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-weight: 900; + font-size: 17px; +} + +.messenger-chat-meta { + margin-top: 3px; + color: #6b7280; + font-size: 13px; +} + +.messenger-ai-summary { + margin: 12px 16px 0; +} + +.messenger-error { + margin: 12px 16px 0; + padding: 12px 14px; + border: 1px solid #fecaca; + border-radius: 14px; + background: #fff1f2; + color: #991b1b; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.messenger-chat-surface { + flex: 1; + min-height: 0; + padding: 16px; + overflow: hidden; +} + +.messenger-message-timeline { + height: 100%; + border-radius: 18px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.58), rgba(255, 255, 255, 0.18)), + repeating-linear-gradient(45deg, rgba(51, 102, 232, 0.04) 0 10px, transparent 10px 20px); + align-content: start; +} + +.messenger-composer-wrap { + padding: 0 16px 16px; + background: rgba(231, 237, 245, 0.84); +} + +.messenger-composer-shell { + grid-template-columns: minmax(0, 1fr) 48px; + max-width: none; + align-items: center; + background: #fff; + border: 1px solid #dce5f0; + box-shadow: 0 12px 30px rgba(16, 33, 61, 0.08); +} + +.messenger-composer-shell textarea { + min-height: 42px; + max-height: 112px; + height: 42px; + padding: 10px 8px; + border: 0; + background: transparent; + resize: none; + outline: none; + font-size: 15px; + line-height: 1.45; + overflow-y: auto; +} + +.messenger-composer-shell textarea:disabled { + background: #f6f8fb; + color: #94a3b8; +} + +.messenger-context-card { + border: 1px solid #dbe5f2; + border-radius: 18px; + background: #fff; + box-shadow: 0 12px 28px rgba(16, 33, 61, 0.05); + padding: 16px; + margin-bottom: 14px; +} + +.messenger-context-card h3 { + margin: 4px 0 14px; + font-size: 18px; +} + +.messenger-context-card.compact { + box-shadow: none; +} + +.messenger-context-list { + display: grid; + gap: 10px; + margin: 0 0 16px; +} + +.messenger-context-list div { + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + gap: 10px; +} + +.messenger-context-list dt { + color: #8390a3; + font-size: 12px; + font-weight: 900; + text-transform: uppercase; +} + +.messenger-context-list dd { + margin: 0; + min-width: 0; + overflow-wrap: anywhere; + color: #10213d; + font-weight: 800; +} + +.messenger-context-preview { + margin: 4px 0 0; + color: #536179; + line-height: 1.45; +} + +.telegram-panel { + padding: 0; + overflow: hidden; + background: #e7ebf0; + height: calc(100dvh - 152px); + min-height: 620px; + display: flex; +} + +.telegram-layout { + width: 100%; + height: 100%; + display: grid; + grid-template-columns: 400px minmax(0, 1fr); + min-height: 0; +} + +.telegram-threads-pane { + background: #fff; + border-right: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.telegram-pane-head { + padding: 12px; + display: flex; + align-items: center; + gap: 12px; +} + +.telegram-pane-icon-btn, +.telegram-chat-icon-btn, +.telegram-composer-icon-btn, +.telegram-send-btn { + border: 0; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #707579; +} + +.telegram-pane-icon-btn, +.telegram-chat-icon-btn, +.telegram-composer-icon-btn { + width: 40px; + height: 40px; + border-radius: 999px; +} + +.telegram-pane-icon-btn:hover, +.telegram-chat-icon-btn:hover, +.telegram-composer-icon-btn:hover { + background: #f3f4f6; + color: #0088cc; +} + +.telegram-chat-icon-btn.active { + background: #e6f4fb; + color: #0088cc; +} + +.telegram-pane-icon-btn:disabled { + cursor: default; + opacity: 1; +} + +.telegram-pane-icon-btn svg, +.telegram-chat-icon-btn svg, +.telegram-thread-search-icon svg, +.telegram-composer-icon-btn svg, +.telegram-send-btn svg { + width: 20px; + height: 20px; + fill: none; + stroke: currentColor; + stroke-width: 1.9; + stroke-linecap: round; + stroke-linejoin: round; +} + +.telegram-thread-search { + position: relative; + flex: 1; +} + +.telegram-thread-search input { + width: 100%; + height: 44px; + padding: 0 16px 0 42px; + border: 0; + border-radius: 999px; + background: #f4f4f5; + color: #000; + font-size: 14px; + outline: none; +} + +.telegram-thread-search input:focus { + box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.25); +} + +.telegram-thread-search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: #9ca3af; + pointer-events: none; + display: inline-flex; +} + +.telegram-thread-list { + flex: 1; + overflow: auto; + padding-bottom: 10px; +} + +.telegram-empty-state { + min-height: 120px; + display: grid; + place-items: center; + text-align: center; + color: #707579; +} + +.telegram-thread-card { + width: 100%; + padding: 12px 16px; + border: 0; + background: transparent; + color: #000; + text-align: left; + cursor: pointer; + display: flex; + align-items: center; + gap: 12px; + transition: background 0.16s ease, color 0.16s ease; +} + +.telegram-thread-card:hover { + background: #f9fafb; +} + +.telegram-thread-card.selected { + background: #0088cc; + color: #fff; +} + +.telegram-thread-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(135deg, var(--telegram-avatar-start, #4daeff), var(--telegram-avatar-end, #228fff)); + color: #fff; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 17px; + font-weight: 800; + flex: 0 0 auto; +} + +.telegram-thread-card.selected .telegram-thread-avatar { + background: rgba(255, 255, 255, 0.2); +} + +.telegram-thread-copy, +.telegram-thread-body { + min-width: 0; + flex: 1; +} + +.telegram-thread-card-head, +.telegram-thread-preview-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.telegram-thread-status-row, +.telegram-thread-meta-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.telegram-thread-name { + font-size: 15px; + font-weight: 800; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.telegram-thread-time { + color: #9ca3af; + font-size: 12px; + white-space: nowrap; +} + +.telegram-thread-preview { + min-width: 0; + color: #707579; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.telegram-thread-summary { + min-width: 0; + color: #8a94a6; + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.telegram-thread-card.selected .telegram-thread-time { + color: rgba(255, 255, 255, 0.72); +} + +.telegram-thread-card.selected .telegram-thread-preview { + color: rgba(255, 255, 255, 0.82); +} + +.telegram-thread-card.selected .telegram-thread-summary { + color: rgba(255, 255, 255, 0.72); +} + +.telegram-thread-card.selected .telegram-thread-ai-chip { + background: rgba(255, 255, 255, 0.18); + color: #fff; +} + +.telegram-thread-unread { + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #0088cc; + color: #fff; + font-size: 11px; + font-weight: 800; + flex: 0 0 auto; +} + +.telegram-chat-pane { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + overflow: hidden; + background: #e7ebf0; +} + +.telegram-chat-header { + height: 56px; + padding: 0 16px; + background: #fff; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06); + position: sticky; + top: 0; + z-index: 2; + flex: 0 0 auto; +} + +.telegram-thread-identity { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.telegram-thread-title { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 16px; + font-weight: 800; + line-height: 1.15; +} + +.telegram-thread-meta { + margin-top: 2px; + color: #0088cc; + font-size: 13px; + line-height: 1.2; +} + +.telegram-thread-ai-chip { + min-height: 24px; + padding: 0 10px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.02em; + background: #edf2f7; + color: #64748b; +} + +.telegram-thread-ai-chip.active, +.telegram-thread-ai-chip.queued, +.telegram-thread-ai-chip.thinking { + background: #e8f1ff; + color: #1f6feb; +} + +.telegram-thread-ai-chip.handoff { + background: #fff3d6; + color: #a15c00; +} + +.telegram-thread-ai-chip.human { + background: #e7f7ef; + color: #0f9f6f; +} + +.telegram-thread-ai-chip.closed { + background: #eef2f7; + color: #667085; +} + +.telegram-thread-ai-chip.error { + background: #ffe8e8; + color: #c62828; +} + +.telegram-chat-actions { + display: flex; + align-items: center; + gap: 2px; + flex: 0 0 auto; +} + +.telegram-message-search { + padding: 10px 16px; + background: rgba(255, 255, 255, 0.92); + border-bottom: 1px solid rgba(209, 213, 219, 0.7); + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + flex: 0 0 auto; +} + +.telegram-message-search-field { + position: relative; + flex: 1; +} + +.telegram-message-search-field input { + width: 100%; + height: 40px; + padding: 0 14px 0 42px; + border: 0; + border-radius: 14px; + background: #f4f4f5; + color: #000; + font-size: 14px; + outline: none; +} + +.telegram-message-search-field input:focus { + box-shadow: 0 0 0 2px rgba(0, 136, 204, 0.25); +} + +.telegram-message-search-meta { + color: #707579; + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.telegram-message-search-clear { + min-height: 34px; + padding: 0 14px; + border: 0; + border-radius: 999px; + background: #eef6fb; + color: #0088cc; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 13px; + font-weight: 700; + cursor: pointer; +} + +.telegram-message-search-clear:hover { + background: #dff0fa; +} + +.telegram-operator-tray { + padding: 12px 16px; + background: rgba(255, 255, 255, 0.88); + border-bottom: 1px solid rgba(209, 213, 219, 0.7); + display: grid; + gap: 10px; + flex: 0 0 auto; +} + +.telegram-operator-meta, +.telegram-operator-controls { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.telegram-operator-chip { + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; +} + +.telegram-operator-chip.claimed { + background: #e7f7ef; + color: #0f9f6f; +} + +.telegram-operator-chip.free { + background: #eef2f7; + color: #667085; +} + +.telegram-operator-chip.active, +.telegram-operator-chip.queued, +.telegram-operator-chip.thinking { + background: #e8f1ff; + color: #1f6feb; +} + +.telegram-operator-chip.handoff { + background: #fff3d6; + color: #a15c00; +} + +.telegram-operator-chip.human { + background: #e7f7ef; + color: #0f9f6f; +} + +.telegram-operator-chip.closed { + background: #eef2f7; + color: #667085; +} + +.telegram-operator-chip.error { + background: #ffe8e8; + color: #c62828; +} + +.telegram-operator-chip.subtle { + background: #f4f4f5; + color: #707579; +} + +.telegram-operator-action { + min-height: 34px; + padding: 0 14px; + border: 0; + border-radius: 999px; + background: #0088cc; + color: #fff; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 13px; + font-weight: 700; + cursor: pointer; +} + +.telegram-operator-action.subtle { + background: #f4f4f5; + color: #374151; +} + +.telegram-operator-action:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.telegram-operator-input { + min-width: 120px; + height: 34px; + padding: 0 12px; + border: 1px solid #d1d5db; + border-radius: 999px; + background: #fff; +} + +.telegram-ai-summary { + padding: 14px 16px 10px; + background: rgba(255, 255, 255, 0.94); + border-bottom: 1px solid rgba(209, 213, 219, 0.72); + display: grid; + gap: 12px; + flex: 0 0 auto; +} + +.telegram-ai-summary-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.telegram-ai-summary-title { + color: #10213d; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 13px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.telegram-ai-summary-caption, +.telegram-ai-summary-meta { + color: #707579; + font-size: 12px; + line-height: 1.45; +} + +.telegram-ai-summary-meta { + white-space: nowrap; + text-align: right; +} + +.telegram-ai-summary-side { + display: grid; + justify-items: end; + gap: 8px; +} + +.telegram-ai-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.telegram-summary-field { + padding: 12px 14px; + border-radius: 16px; + border: 1px solid rgba(219, 228, 241, 0.92); + background: #fff; + box-shadow: 0 8px 20px rgba(16, 33, 61, 0.04); + display: grid; + gap: 6px; +} + +.telegram-summary-label { + color: #6b7280; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.telegram-summary-value { + color: #10213d; + font-size: 14px; + line-height: 1.55; + white-space: pre-wrap; +} + +.telegram-summary-status { + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.telegram-summary-status.answered { + background: #e7f7ef; + color: #0f9f6f; +} + +.telegram-summary-status.handoff { + background: #fff3d6; + color: #a15c00; +} + +.telegram-chat-surface { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + background: #e2e5ea; +} + +.telegram-message-timeline { + position: relative; + z-index: 1; + height: 100%; + overflow: auto; + padding: 16px; + display: grid; + gap: 8px; + align-content: start; +} + +.telegram-message { + display: flex; + width: 100%; + align-items: flex-start; +} + +.telegram-message.me { + justify-content: flex-end; +} + +.telegram-message.other { + justify-content: flex-start; +} + +.telegram-message.system { + justify-content: center; +} + +.telegram-message-bubble { + position: relative; + width: fit-content; + min-width: 92px; + max-width: min(85%, 720px); + padding: 10px 12px 20px; + border-radius: 18px; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06); +} + +.telegram-message-bubble.me { + background: #effdde; + color: #000; + border-top-right-radius: 6px; +} + +.telegram-message-bubble.ai { + box-shadow: 0 3px 10px rgba(0, 136, 204, 0.12); +} + +.telegram-message-bubble.other { + background: #fff; + color: #000; + border-top-left-radius: 6px; +} + +.telegram-message-system-pill { + padding: 8px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.8); + color: #707579; + font-size: 12px; + font-weight: 700; +} + +.telegram-message-body { + padding-right: 52px; + white-space: pre-wrap; + line-height: 1.5; + font-size: 15px; + color: inherit; + overflow-wrap: anywhere; +} + +.telegram-message-badge { + display: inline-flex; + align-items: center; + min-height: 20px; + padding: 0 8px; + border-radius: 999px; + margin-bottom: 8px; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.telegram-message-badge.ai { + background: rgba(0, 136, 204, 0.14); + color: #0088cc; +} + +.telegram-message-meta { + position: absolute; + right: 10px; + bottom: 6px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.telegram-message-time { + font-size: 10px; + color: #9ca3af; +} + +.telegram-message-bubble.me .telegram-message-time { + color: rgba(21, 128, 61, 0.72); +} + +.telegram-message-checks { + font-size: 13px; + line-height: 1; + color: #22c55e; +} + +.telegram-message-highlight { + padding: 0 2px; + border-radius: 5px; + background: #fff1a8; + color: inherit; +} + +.telegram-composer-wrap { + padding: 10px 16px 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: sticky; + bottom: 0; + z-index: 2; + background: #e7ebf0; + flex: 0 0 auto; +} + +.telegram-composer-shell { + width: 100%; + max-width: 860px; + display: grid; + grid-template-columns: 44px minmax(0, 1fr) 44px 48px; + align-items: end; + gap: 4px; + padding: 6px; + background: #fff; + border-radius: 18px; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08); +} + +.telegram-composer-icon-btn { + color: #9ca3af; +} + +.telegram-composer-icon-btn:disabled { + cursor: default; + opacity: 1; +} + +.telegram-composer-shell textarea { + min-height: 48px; + max-height: 128px; + height: 48px; + padding: 10px 4px; + border: 0; + background: transparent; + resize: none; + outline: none; + font-size: 16px; + line-height: 1.45; +} + +.telegram-send-btn { + width: 44px; + height: 44px; + border-radius: 999px; + color: #0088cc; + transform: scale(0.92); + transition: transform 0.16s ease, background 0.16s ease, color 0.16s ease; +} + +.telegram-send-btn:not(:disabled) { + background: #0088cc; + color: #fff; + transform: scale(1); +} + +.telegram-send-btn:disabled { + cursor: not-allowed; +} + +.telegram-composer-hint { + color: #707579; + font-size: 12px; + text-align: center; +} + +.whatsapp-panel { + padding: 0; + overflow: hidden; + background: #0b141a; + height: calc(100dvh - 152px); + min-height: 620px; + display: flex; +} + +.whatsapp-panel, +.whatsapp-panel button, +.whatsapp-panel input, +.whatsapp-panel textarea { + font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +} + +.whatsapp-shell { + width: 100%; + height: 100%; + display: grid; + grid-template-columns: 72px 420px minmax(0, 1fr); + min-height: 0; + background: #0b141a; + color: #e9edef; +} + +.whatsapp-panel svg { + width: 20px; + height: 20px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.whatsapp-nav-rail { + padding: 14px 0; + background: #202c33; + border-right: 1px solid rgba(233, 237, 239, 0.08); + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.whatsapp-rail-group, +.whatsapp-rail-footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.whatsapp-rail-btn, +.whatsapp-icon-btn, +.whatsapp-composer-btn, +.whatsapp-send-btn { + border: 0; + background: transparent; + color: #aebac1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.16s ease, color 0.16s ease, transform 0.16s ease; +} + +.whatsapp-rail-btn, +.whatsapp-icon-btn, +.whatsapp-composer-btn, +.whatsapp-send-btn { + width: 42px; + height: 42px; + border-radius: 14px; +} + +.whatsapp-rail-btn:hover, +.whatsapp-icon-btn:hover, +.whatsapp-composer-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: #f7f9fa; +} + +.whatsapp-rail-btn.active { + background: rgba(37, 211, 102, 0.18); + color: #25d366; +} + +.whatsapp-rail-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(135deg, #25d366, #128c7e); + color: #08171d; + font-size: 13px; + font-weight: 800; +} + +.whatsapp-list-pane { + background: #111b21; + border-right: 1px solid rgba(233, 237, 239, 0.08); + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.whatsapp-list-head { + padding: 18px 16px 10px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.whatsapp-pane-title-wrap { + min-width: 0; +} + +.whatsapp-pane-title { + font-size: 19px; + font-weight: 700; + letter-spacing: -0.01em; + color: #e9edef; +} + +.whatsapp-pane-caption { + margin-top: 4px; + color: #8696a0; + font-size: 12px; +} + +.whatsapp-list-actions, +.whatsapp-chat-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.whatsapp-icon-btn { + color: #8696a0; +} + +.whatsapp-search-wrap { + padding: 0 16px 12px; + display: grid; + gap: 10px; +} + +.whatsapp-search-field { + position: relative; +} + +.whatsapp-search-icon { + position: absolute; + left: 14px; + top: 50%; + transform: translateY(-50%); + color: #8696a0; + display: inline-flex; + pointer-events: none; +} + +.whatsapp-search-field input { + width: 100%; + height: 42px; + padding: 0 16px 0 42px; + border: 1px solid transparent; + border-radius: 12px; + background: #202c33; + color: #e9edef; + font-size: 14px; + outline: none; +} + +.whatsapp-search-field input::placeholder, +.whatsapp-composer-shell textarea::placeholder { + color: #8696a0; +} + +.whatsapp-search-field input:focus { + border-color: rgba(37, 211, 102, 0.45); +} + +.whatsapp-filter-bar { + display: flex; + align-items: center; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; + scrollbar-width: none; +} + +.whatsapp-filter-bar::-webkit-scrollbar { + display: none; +} + +.whatsapp-filter-chip { + min-height: 32px; + padding: 0 12px; + border: 1px solid rgba(233, 237, 239, 0.08); + border-radius: 999px; + background: #202c33; + color: #aebac1; + font-size: 13px; + font-weight: 600; + cursor: pointer; + flex: 0 0 auto; +} + +.whatsapp-filter-chip.active { + background: rgba(37, 211, 102, 0.18); + border-color: rgba(37, 211, 102, 0.18); + color: #d9fdd3; +} + +.whatsapp-chat-list { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding-bottom: 12px; +} + +.whatsapp-chat-list-empty, +.whatsapp-chat-empty { + min-height: 180px; + display: grid; + place-items: center; + text-align: center; + color: #aebac1; + padding: 24px; +} + +.whatsapp-chat-list-empty strong, +.whatsapp-chat-empty strong { + display: block; + color: #e9edef; + margin-bottom: 8px; +} + +.whatsapp-chat-card { + width: 100%; + padding: 14px 16px; + border: 0; + background: transparent; + color: inherit; + text-align: left; + display: flex; + align-items: flex-start; + gap: 12px; + cursor: pointer; + transition: background 0.16s ease; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.whatsapp-chat-card:hover { + background: rgba(255, 255, 255, 0.04); +} + +.whatsapp-chat-card.selected { + background: #202c33; +} + +.whatsapp-chat-card-avatar, +.whatsapp-chat-avatar { + border-radius: 50%; + display: grid; + place-items: center; + background: linear-gradient(135deg, var(--whatsapp-avatar-start, #25d366), var(--whatsapp-avatar-end, #128c7e)); + color: #fff; + font-size: 15px; + font-weight: 700; + flex: 0 0 auto; +} + +.whatsapp-chat-card-avatar { + width: 49px; + height: 49px; +} + +.whatsapp-chat-card-copy { + min-width: 0; + flex: 1; + display: grid; + gap: 4px; +} + +.whatsapp-chat-card-head, +.whatsapp-chat-card-preview-row, +.whatsapp-chat-card-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.whatsapp-chat-card-title { + min-width: 0; + font-size: 15px; + font-weight: 600; + color: #e9edef; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.whatsapp-chat-card-time { + color: #8696a0; + font-size: 12px; + white-space: nowrap; +} + +.whatsapp-chat-card-preview { + min-width: 0; + color: #aebac1; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.whatsapp-chat-card-meta { + justify-content: flex-start; + flex-wrap: wrap; + color: #8696a0; + font-size: 11px; +} + +.whatsapp-chat-tag { + min-height: 20px; + padding: 0 8px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.06); + color: #cfd8dc; + display: inline-flex; + align-items: center; +} + +.whatsapp-chat-tag.ai.active, +.whatsapp-chat-tag.ai.queued, +.whatsapp-chat-tag.ai.thinking { + background: rgba(83, 189, 235, 0.18); + color: #d2f4ff; +} + +.whatsapp-chat-tag.ai.handoff { + background: rgba(255, 205, 96, 0.18); + color: #ffe5a6; +} + +.whatsapp-chat-tag.ai.human { + background: rgba(37, 211, 102, 0.18); + color: #d9fdd3; +} + +.whatsapp-chat-tag.ai.closed { + background: rgba(255, 255, 255, 0.08); + color: #d5dde0; +} + +.whatsapp-chat-tag.ai.error { + background: rgba(255, 118, 117, 0.18); + color: #ffd3d1; +} + +.whatsapp-chat-tag.owner { + background: rgba(255, 255, 255, 0.08); + color: #f7f9fa; +} + +.whatsapp-chat-unread { + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #25d366; + color: #08171d; + font-size: 11px; + font-weight: 800; +} + +.whatsapp-chat-pane { + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + background: #efeae2; +} + +.whatsapp-chat-head { + min-height: 72px; + padding: 12px 18px; + background: #f0f2f5; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + border-bottom: 1px solid rgba(17, 27, 33, 0.08); + flex: 0 0 auto; +} + +.whatsapp-context-panel { + padding: 10px 16px 0; + background: #f0f2f5; + border-bottom: 1px solid rgba(17, 27, 33, 0.08); + display: grid; + gap: 10px; +} + +.whatsapp-context-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.whatsapp-context-pills, +.whatsapp-context-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.whatsapp-state-chip, +.whatsapp-summary-status { + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; +} + +.whatsapp-state-chip.active, +.whatsapp-state-chip.queued, +.whatsapp-state-chip.thinking { + background: #dff2fc; + color: #126c89; +} + +.whatsapp-state-chip.handoff, +.whatsapp-summary-status.handoff { + background: #fff0cf; + color: #8d5c00; +} + +.whatsapp-state-chip.human, +.whatsapp-summary-status.answered { + background: #d9fdd3; + color: #0f7a45; +} + +.whatsapp-state-chip.closed { + background: #e5eaed; + color: #54656f; +} + +.whatsapp-state-chip.error { + background: #ffe3e1; + color: #c03b37; +} + +.whatsapp-state-chip.subtle { + background: rgba(17, 27, 33, 0.06); + color: #54656f; +} + +.whatsapp-context-btn { + min-height: 34px; + padding: 0 14px; + border: 0; + border-radius: 999px; + background: #25d366; + color: #08171d; + font-size: 13px; + font-weight: 700; + cursor: pointer; +} + +.whatsapp-context-btn.secondary { + background: rgba(17, 27, 33, 0.08); + color: #111b21; +} + +.whatsapp-context-btn:disabled { + background: #dfe5e7; + color: #94a3a8; + cursor: not-allowed; +} + +.whatsapp-ai-summary { + padding: 14px; + border-radius: 18px; + border: 1px solid rgba(17, 27, 33, 0.08); + background: rgba(255, 255, 255, 0.84); + box-shadow: 0 10px 30px rgba(17, 27, 33, 0.06); +} + +.whatsapp-ai-summary-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.whatsapp-ai-summary-kicker { + color: #667781; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.whatsapp-ai-summary-head h3 { + margin: 6px 0 0; + color: #111b21; + font-size: 18px; + font-weight: 700; +} + +.whatsapp-ai-summary-side { + display: grid; + justify-items: end; + gap: 8px; +} + +.whatsapp-ai-summary-meta { + color: #667781; + font-size: 12px; + white-space: nowrap; +} + +.whatsapp-ai-summary-grid { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.whatsapp-summary-field { + padding: 12px 14px; + border-radius: 16px; + border: 1px solid rgba(17, 27, 33, 0.08); + background: rgba(240, 242, 245, 0.78); +} + +.whatsapp-summary-label { + color: #667781; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.whatsapp-summary-value { + margin-top: 8px; + color: #111b21; + font-size: 14px; + line-height: 1.55; +} + +.whatsapp-chat-identity { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.whatsapp-chat-avatar { + width: 40px; + height: 40px; + font-size: 14px; +} + +.whatsapp-chat-copy { + min-width: 0; +} + +.whatsapp-chat-title { + font-size: 16px; + font-weight: 600; + color: #111b21; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.whatsapp-chat-meta { + margin-top: 4px; + color: #667781; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.whatsapp-chat-surface { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + background: + radial-gradient(circle at top left, rgba(18, 140, 126, 0.06), transparent 22%), + radial-gradient(circle at bottom right, rgba(37, 211, 102, 0.08), transparent 18%), + #efeae2; +} + +.whatsapp-chat-surface::before { + content: ""; + position: absolute; + inset: 0; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.18) 12%, transparent 12%) 0 0 / 28px 28px, + linear-gradient(45deg, rgba(255, 255, 255, 0.12) 12%, transparent 12%) 0 0 / 28px 28px; + opacity: 0.28; + pointer-events: none; +} + +.whatsapp-message-timeline { + position: relative; + z-index: 1; + height: 100%; + overflow: auto; + padding: 20px min(5vw, 32px) 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.whatsapp-encryption-pill, +.whatsapp-day-pill { + align-self: center; + display: inline-flex; + align-items: center; + justify-content: center; + width: fit-content; + max-width: min(92%, 640px); + padding: 7px 12px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.72); + color: #54656f; + font-size: 12px; + line-height: 1.45; + text-align: center; + box-shadow: 0 1px 2px rgba(17, 27, 33, 0.08); +} + +.whatsapp-day-pill { + background: rgba(223, 242, 252, 0.92); + color: #54656f; + font-weight: 600; +} + +.whatsapp-message-row { + display: flex; + width: 100%; + min-width: 0; +} + +.whatsapp-message-row.me { + justify-content: flex-end; +} + +.whatsapp-message-row.other { + justify-content: flex-start; +} + +.whatsapp-message-bubble { + position: relative; + width: fit-content; + max-width: min(72%, 720px); + min-width: 120px; + padding: 8px 12px 22px; + border-radius: 12px; + box-shadow: 0 1px 2px rgba(17, 27, 33, 0.12); +} + +.whatsapp-message-bubble.me { + background: #d9fdd3; + color: #111b21; + border-top-right-radius: 4px; +} + +.whatsapp-message-bubble.other { + background: #fff; + color: #111b21; + border-top-left-radius: 4px; +} + +.whatsapp-message-author { + margin-bottom: 4px; + color: #0b7285; + font-size: 12px; + font-weight: 700; +} + +.whatsapp-message-badge { + margin-bottom: 6px; + display: inline-flex; + align-items: center; + min-height: 20px; + padding: 0 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.whatsapp-message-badge.ai { + background: rgba(18, 108, 137, 0.12); + color: #126c89; +} + +.whatsapp-message-body { + padding-right: 56px; + white-space: pre-wrap; + overflow-wrap: anywhere; + font-size: 14px; + line-height: 1.45; +} + +.whatsapp-message-meta { + position: absolute; + right: 10px; + bottom: 6px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.whatsapp-message-time { + font-size: 11px; + color: #667781; +} + +.whatsapp-message-checks { + font-size: 12px; + line-height: 1; + color: #667781; +} + +.whatsapp-message-checks.read { + color: #53bdeb; +} + +.whatsapp-message-checks.delivered { + color: #667781; +} + +.whatsapp-composer-wrap { + padding: 10px 16px 16px; + background: #f0f2f5; + flex: 0 0 auto; +} + +.whatsapp-composer-shell { + display: grid; + grid-template-columns: 40px minmax(0, 1fr) 40px 44px; + align-items: end; + gap: 8px; + padding: 8px 10px; + border-radius: 16px; + background: #fff; + box-shadow: 0 1px 2px rgba(17, 27, 33, 0.08); +} + +.whatsapp-composer-btn { + color: #54656f; +} + +.whatsapp-composer-shell textarea { + min-height: 24px; + max-height: 120px; + height: 24px; + padding: 8px 2px; + border: 0; + background: transparent; + resize: none; + outline: none; + color: #111b21; + font-size: 15px; + line-height: 1.45; +} + +.whatsapp-send-btn { + border-radius: 50%; + background: #25d366; + color: #08171d; +} + +.whatsapp-send-btn:disabled { + background: #dfe5e7; + color: #94a3a8; + cursor: not-allowed; +} + +.customers-summary { + color: var(--text-soft); + font-size: 14px; +} + +.table { + display: grid; + gap: 12px; + max-height: 420px; + overflow: auto; +} + +.pipeline-board { + display: grid; + grid-template-columns: repeat(4, minmax(240px, 1fr)); + gap: 14px; + align-items: start; +} + +.unified-inbox-panel { + display: grid; + gap: 16px; +} + +.unified-inbox-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.unified-inbox-scroll { + overflow-x: auto; + padding-bottom: 4px; +} + +.unified-inbox-board { + grid-template-columns: repeat(5, minmax(240px, 1fr)); + min-width: 1320px; +} + +.unified-inbox-card { + min-height: 248px; +} + +.live-calls-board { + grid-template-columns: repeat(2, minmax(280px, 1fr)); +} + +.pipeline-column { + min-width: 0; + padding: 12px; + border-radius: 20px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #f8faff, #f2f6fc); +} + +.pipeline-column-recent { + background: linear-gradient(180deg, #fbfcfe, #f5f7fb); +} + +.pipeline-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 12px; +} + +.pipeline-title { + font-size: 18px; + font-weight: 800; + letter-spacing: -0.02em; +} + +.pipeline-count { + min-width: 30px; + height: 30px; + padding: 0 10px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #e1e8f4; + color: var(--text-soft); + font-size: 13px; + font-weight: 800; +} + +.pipeline-stack { + display: grid; + gap: 10px; +} + +.pipeline-card { + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line); + background: #fff; + box-shadow: 0 10px 24px rgba(16, 33, 61, 0.05); +} + +.pipeline-card.escalated { + border-left: 4px solid var(--danger); +} + +.pipeline-card.closed { + border-left: 4px solid #d9e0ea; +} + +.live-call-card.selected { + border-color: #b9c9ef; + box-shadow: 0 14px 26px rgba(51, 102, 232, 0.12); +} + +.live-call-card-actions { + margin-top: 14px; + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.live-call-card-actions .btn { + min-height: 38px; + padding: 0 14px; + font-size: 13px; +} + +.live-call-name-editor { + margin: 14px 0; + padding: 14px; + border-radius: 18px; + border: 1px solid var(--primary-line); + background: linear-gradient(180deg, #ffffff, #f8fbff); + display: grid; + gap: 10px; +} + +.live-call-name-editor-head { + display: grid; + gap: 4px; +} + +.live-call-name-editor-title { + font-size: 15px; + font-weight: 800; + color: var(--text); +} + +.live-call-name-editor-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; +} + +.live-call-name-editor-form input { + min-width: 0; +} + +.hint.error, +.call-window-meta.error { + color: var(--danger); +} + +.card-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 10px; +} + +.micro-badge { + min-height: 24px; + padding: 0 8px; + border-radius: 8px; + display: inline-flex; + align-items: center; + border: 1px solid transparent; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.micro-badge.channel { + background: var(--primary-soft); + border-color: var(--primary-line); + color: var(--primary); +} + +.micro-badge.facebook { + background: #e7efff; + border-color: #bfd2ff; + color: #1657d8; +} + +.micro-badge.queue { + background: #f4f6fa; + border-color: #dde4ef; + color: var(--text-soft); +} + +.micro-badge.assignee { + background: var(--success-soft); + border-color: #bee8d3; + color: var(--success); +} + +.micro-badge.owner { + background: #fff4df; + border-color: #f1d8a3; + color: #9a6a00; +} + +.micro-badge.terminal { + background: #edf2fb; + border-color: #d9e1f1; + color: #51627e; +} + +.micro-badge.ai-active { + background: rgba(67, 200, 152, 0.14); + border-color: rgba(67, 200, 152, 0.34); + color: #118a63; +} + +.micro-badge.ai-thinking { + background: rgba(64, 132, 255, 0.12); + border-color: rgba(64, 132, 255, 0.28); + color: #3466d6; +} + +.micro-badge.ai-handoff { + background: rgba(245, 158, 11, 0.14); + border-color: rgba(245, 158, 11, 0.34); + color: #b66b00; +} + +.micro-badge.ai-human { + background: rgba(148, 163, 184, 0.16); + border-color: rgba(148, 163, 184, 0.32); + color: #55657a; +} + +.micro-badge.ai-error { + background: rgba(239, 68, 68, 0.14); + border-color: rgba(239, 68, 68, 0.32); + color: #cc4343; +} + +.micro-badge.ai-muted { + background: rgba(226, 232, 240, 0.4); + border-color: rgba(203, 213, 225, 0.62); + color: #6b7280; +} + +.micro-badge.name-confirmed { + background: rgba(16, 185, 129, 0.14); + border-color: rgba(16, 185, 129, 0.28); + color: #117c5f; +} + +.micro-badge.name-followup { + background: rgba(245, 158, 11, 0.14); + border-color: rgba(245, 158, 11, 0.28); + color: #b66900; +} + +.micro-badge.name-missing { + background: rgba(148, 163, 184, 0.16); + border-color: rgba(148, 163, 184, 0.3); + color: #55657a; +} + +.card-title { + margin: 0; + font-size: 18px; + line-height: 1.25; + letter-spacing: -0.02em; +} + +.card-subtitle { + margin: 4px 0 10px; + color: var(--text-soft); + font-size: 13px; +} + +.card-meta-line { + margin: 0 0 10px; + color: var(--text-faint); + font-size: 12px; +} + +.card-divider { + height: 1px; + margin: 10px 0 12px; + background: #edf1f7; +} + +.row-item { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fff, #fbfcff); + box-shadow: 0 10px 22px rgba(16, 33, 61, 0.04); +} + +.row-item-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; +} + +.row-item-meta { + margin-bottom: 10px; + color: var(--text-soft); + font-size: 13px; + line-height: 1.5; +} + +.row-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.row-actions button { + min-height: 38px; + padding: 0 12px; + border-radius: 12px; +} + +.status-chip { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: 0 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + border: 1px solid transparent; +} + +.status-new { + background: var(--warning-soft); + border-color: #f7ddb0; + color: #9a6a00; +} + +.status-in-progress { + background: var(--success-soft); + border-color: #bee8d3; + color: var(--success); +} + +.status-escalated { + background: var(--primary-soft); + border-color: var(--primary-line); + color: var(--primary); +} + +.status-closed { + background: #eef2f8; + border-color: #dde4ef; + color: var(--text-soft); +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 12px; +} + +.summary-card { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #ffffff, #f8fbff); +} + +.summary-label { + font-size: 11px; + font-weight: 800; + color: var(--text-faint); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.summary-value { + margin-top: 8px; + font-size: 28px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.summary-note { + margin-top: 5px; + color: var(--text-soft); + font-size: 12px; +} + +.analytics-dashboard-panel { + display: grid; + gap: 18px; + margin-bottom: 18px; +} + +.analytics-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.analytics-head h2 { + margin-bottom: 8px; +} + +.analytics-topbar-intro { + display: grid; + gap: 4px; +} + +.analytics-topbar-intro strong { + font-size: 15px; + font-weight: 800; + color: var(--text); +} + +.analytics-topbar-intro p { + margin: 0; + color: var(--text-soft); + font-size: 13px; +} + +.analytics-filter-bar { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 12px; + align-items: end; +} + +.analytics-toolkit { + display: grid; + grid-template-columns: minmax(220px, 0.8fr) minmax(0, 1.2fr); + gap: 12px; +} + +.analytics-toolkit-group { + display: grid; + gap: 12px; + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #ffffff, #f8fbff); +} + +.analytics-toolkit-group-wide { + grid-template-columns: minmax(180px, 0.9fr) minmax(220px, 1fr) auto; + align-items: end; +} + +.analytics-toolkit-group-wide { + display: grid; + gap: 12px; +} + +.analytics-toolkit-actions { + justify-content: flex-end; +} + +.analytics-inline-meta { + min-height: 20px; +} + +.analytics-filter-note { + margin: -6px 0 0; +} + +.analytics-empty-state { + padding: 14px 16px; + border-radius: 16px; + border: 1px dashed var(--primary-line); + background: linear-gradient(180deg, #f7f9ff, #eef3fb); + color: var(--text-soft); +} + +.analytics-narrative { + display: grid; + gap: 14px; + padding: 18px 20px; + border-radius: 20px; + border: 1px solid var(--primary-line); + background: + radial-gradient(circle at top right, rgba(35, 92, 212, 0.08), transparent 34%), + linear-gradient(180deg, #f8fbff, #eef4ff); +} + +.analytics-narrative-main { + display: grid; + gap: 6px; +} + +.analytics-narrative-main strong { + font-size: 30px; + line-height: 1; + letter-spacing: -0.04em; +} + +.analytics-narrative-main p { + margin: 0; + max-width: 72ch; + color: var(--text-soft); + line-height: 1.5; +} + +.analytics-narrative-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + flex-wrap: wrap; +} + +.analytics-filter-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.analytics-filter-chip { + display: inline-flex; + align-items: center; + padding: 7px 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid var(--line); + color: var(--text-soft); + font-size: 12px; + font-weight: 700; +} + +.analytics-filter-chip-quiet { + background: rgba(255, 255, 255, 0.48); + border-color: rgba(16, 33, 61, 0.08); +} + +.analytics-compare-panel { + display: grid; + gap: 14px; + padding: 18px; + border-radius: 20px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #ffffff, #f6f9ff); +} + +.analytics-compare-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.analytics-compare-head h3 { + margin: 0 0 6px; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 22px; + line-height: 1.08; +} + +.analytics-compare-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.analytics-compare-card { + display: grid; + gap: 12px; + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fbfcff, #f4f7fd); +} + +.analytics-compare-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.analytics-compare-label { + color: var(--text); + font-size: 13px; + font-weight: 800; +} + +.analytics-compare-values { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.analytics-compare-value { + display: grid; + gap: 6px; +} + +.analytics-compare-value small { + color: var(--text-faint); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.analytics-compare-value strong { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 20px; + line-height: 1.05; + letter-spacing: -0.03em; +} + +.analytics-compare-note { + margin: 0; + color: var(--text-soft); + font-size: 13px; + line-height: 1.45; +} + +.analytics-overview-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-bottom: 0; +} + +.analytics-card { + min-height: 148px; +} + +.analytics-card-top { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +.analytics-card-top-meta { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + flex-wrap: wrap; +} + +.analytics-coverage-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + white-space: nowrap; + border: 1px solid transparent; +} + +.analytics-coverage-badge-exact { + color: #0d6b42; + background: rgba(24, 160, 99, 0.12); + border-color: rgba(24, 160, 99, 0.18); +} + +.analytics-coverage-badge-partial { + color: #8b5b00; + background: rgba(240, 176, 67, 0.16); + border-color: rgba(240, 176, 67, 0.2); +} + +.analytics-coverage-badge-unavailable { + color: var(--text-soft); + background: rgba(16, 33, 61, 0.08); + border-color: rgba(16, 33, 61, 0.1); +} + +.analytics-delta { + display: inline-flex; + align-items: center; + padding: 4px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.analytics-delta-positive { + background: var(--success-soft); + color: var(--success); +} + +.analytics-delta-negative { + background: var(--danger-soft); + color: var(--danger); +} + +.analytics-delta-neutral { + background: #f1f4f9; + color: var(--text-soft); +} + +.analytics-sections { + display: grid; + grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); + gap: 14px; +} + +.analytics-subpanel { + min-width: 0; + padding: 18px; + border-radius: 20px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fbfcff, #f5f8fd); +} + +.analytics-subpanel h3 { + margin: 0 0 8px; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 20px; + line-height: 1.1; +} + +.analytics-subpanel .hint { + margin: 0 0 12px; + font-size: 13px; + color: var(--text-soft); +} + +.analytics-subhead { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; + margin-bottom: 12px; +} + +.analytics-trend-selector { + min-width: 160px; +} + +.analytics-chart-shell { + display: grid; + gap: 10px; +} + +.analytics-chart-plot { + position: relative; +} + +.analytics-chart-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: var(--text-soft); + font-size: 13px; +} + +.analytics-chart-meta-values { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; +} + +.analytics-chart-summary.is-hovered { + color: var(--text); + font-weight: 600; +} + +.analytics-chart-tooltip { + position: absolute; + top: 8px; + left: 0; + transform: translateX(-50%); + display: grid; + gap: 2px; + min-width: 104px; + padding: 8px 10px; + border-radius: 12px; + border: 1px solid rgba(53, 99, 233, 0.18); + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 16px 34px rgba(14, 26, 49, 0.12); + pointer-events: none; + z-index: 2; +} + +.analytics-chart-tooltip strong { + color: var(--text); + font-size: 12px; + line-height: 1.2; +} + +.analytics-chart-tooltip span { + color: var(--primary); + font-size: 13px; + font-weight: 700; + line-height: 1.2; +} + +.analytics-chart-svg { + width: 100%; + height: auto; + display: block; +} + +.analytics-grid-line { + stroke: #e2e8f3; + stroke-width: 1; +} + +.analytics-line { + fill: none; + stroke: var(--primary); + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; +} + +.analytics-point { + fill: #ffffff; + stroke: var(--primary); + stroke-width: 2; + transition: fill 120ms ease, stroke-width 120ms ease, transform 120ms ease; + transform-origin: center; +} + +.analytics-point.is-active { + fill: #dbe8ff; + stroke-width: 3; +} + +.analytics-point-hit { + fill: transparent; + stroke: transparent; + cursor: pointer; +} + +.analytics-axis-label { + fill: var(--text-faint); + font-size: 11px; + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; +} + +.analytics-table-shell { + display: grid; + gap: 0; + border-radius: 16px; + border: 1px solid var(--line); + background: #ffffff; + overflow: hidden; +} + +.analytics-table-head, +.analytics-table-row { + display: grid; + gap: 12px; + align-items: center; + padding: 12px 14px; +} + +.analytics-channel-grid { + grid-template-columns: minmax(120px, 1.4fr) repeat(4, minmax(72px, 1fr)); +} + +.analytics-queue-grid { + grid-template-columns: minmax(180px, 2fr) repeat(4, minmax(72px, 1fr)); +} + +.agent-analytics-grid { + grid-template-columns: minmax(170px, 1.6fr) minmax(110px, 0.9fr) minmax(130px, 1.1fr) repeat(4, minmax(78px, 0.75fr)); +} + +.analytics-table-head { + background: var(--bg-soft); + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.analytics-table-row { + border-top: 1px solid var(--line); + background: #ffffff; + text-align: left; +} + +.analytics-table-row small { + display: block; + margin-top: 4px; + color: var(--text-faint); +} + +.analytics-table-button { + width: 100%; + border: 0; + font: inherit; + cursor: pointer; +} + +.analytics-table-button:hover { + background: #f8fbff; +} + +.analytics-table-button.selected { + background: var(--primary-soft); +} + +.analytics-card-button { + width: 100%; + border: 1px solid var(--primary-line); + text-align: left; + cursor: pointer; + background: linear-gradient(180deg, #ffffff, #f6f9ff); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; +} + +.analytics-card-button:hover { + transform: translateY(-1px); + box-shadow: 0 12px 24px rgba(31, 83, 181, 0.08); + border-color: rgba(31, 83, 181, 0.22); +} + +.analytics-card-readonly { + border: 1px dashed var(--line-strong); + background: linear-gradient(180deg, #fbfcff, #f5f7fb); +} + +.analytics-card-coverage-exact { + border-color: rgba(24, 160, 99, 0.18); +} + +.analytics-card-coverage-partial { + border-color: rgba(240, 176, 67, 0.22); + background: linear-gradient(180deg, #fffdf7, #fbf6ea); +} + +.analytics-card-coverage-unavailable { + border-color: rgba(16, 33, 61, 0.12); +} + +.analytics-card-hint { + margin-top: 12px; + color: var(--primary); + font-size: 12px; + line-height: 1.45; + font-weight: 700; +} + +.analytics-card-readonly .analytics-card-hint { + color: var(--text-faint); + font-weight: 600; +} + +.analytics-ai-panel { + display: grid; + gap: 14px; + padding: 20px; + border-radius: 24px; + border: 1px solid rgba(31, 83, 181, 0.12); + background: + radial-gradient(circle at top right, rgba(31, 83, 181, 0.08), transparent 34%), + linear-gradient(180deg, #f9fbff, #f1f6ff); +} + +.analytics-voice-panel { + display: grid; + gap: 14px; + padding: 20px; + border-radius: 24px; + border: 1px solid rgba(40, 116, 78, 0.12); + background: + radial-gradient(circle at top right, rgba(40, 116, 78, 0.08), transparent 34%), + linear-gradient(180deg, #fbfefb, #f3faf5); +} + +.voice-name-analytics-overview { + margin-bottom: 0; +} + +.voice-name-card { + border-style: solid; + border-color: rgba(40, 116, 78, 0.12); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(244, 250, 246, 0.95)); +} + +.voice-name-funnel-grid { + grid-template-columns: minmax(180px, 1.4fr) repeat(2, minmax(72px, 0.8fr)); +} + +.voice-name-language-grid { + grid-template-columns: minmax(110px, 1fr) repeat(5, minmax(82px, 0.8fr)); +} + +.voice-name-queue-grid { + grid-template-columns: minmax(170px, 1.4fr) repeat(5, minmax(82px, 0.8fr)); +} + +.voice-name-handoff-grid { + grid-template-columns: minmax(180px, 1.4fr) repeat(2, minmax(82px, 0.8fr)); +} + +.analytics-agent-panel { + display: grid; + gap: 14px; + padding: 20px; + border-radius: 24px; + border: 1px solid rgba(16, 33, 61, 0.08); + background: + radial-gradient(circle at top right, rgba(16, 33, 61, 0.05), transparent 34%), + linear-gradient(180deg, #fbfcff, #f4f7fd); +} + +.agent-analytics-overview { + margin-bottom: 0; +} + +.agent-analytics-card { + border-style: solid; +} + +.agent-analytics-state-strip { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.agent-team-grid { + grid-template-columns: minmax(160px, 1.5fr) repeat(5, minmax(72px, 0.9fr)); +} + +.agent-shift-grid { + grid-template-columns: minmax(120px, 1.1fr) repeat(5, minmax(72px, 0.9fr)); +} + +.agent-state-chip { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 800; + border: 1px solid rgba(16, 33, 61, 0.08); + background: rgba(255, 255, 255, 0.94); + color: var(--text-soft); +} + +.agent-state-chip-ready { + background: rgba(24, 166, 109, 0.14); + color: #16694a; +} + +.agent-state-chip-busy { + background: rgba(255, 177, 46, 0.18); + color: #9c5b00; +} + +.agent-state-chip-break { + background: rgba(123, 66, 200, 0.14); + color: #6a34ad; +} + +.agent-state-chip-offline { + background: rgba(16, 33, 61, 0.08); + color: var(--text-soft); +} + +.ai-analytics-overview { + margin-bottom: 0; +} + +.ai-analytics-card { + border-style: solid; + border-color: rgba(31, 83, 181, 0.12); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(240, 246, 255, 0.95)); +} + +.ai-analytics-card-hint { + color: var(--text-soft); +} + +.ai-analytics-channel-grid { + grid-template-columns: minmax(120px, 1.3fr) repeat(5, minmax(72px, 1fr)); +} + +.ai-analytics-outcome-grid { + grid-template-columns: minmax(160px, 1.4fr) repeat(2, minmax(72px, 0.8fr)); +} + +.ai-analytics-reason-grid { + grid-template-columns: minmax(220px, 1.6fr) repeat(2, minmax(72px, 0.8fr)); +} + +.ai-analytics-coverage-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.ai-analytics-row-button { + width: 100%; + font: inherit; + text-align: left; + cursor: pointer; +} + +.ai-analytics-row-button:hover { + border-color: rgba(31, 83, 181, 0.2); + background: linear-gradient(180deg, #ffffff, #f1f6ff); +} + +.ai-analytics-row-button:focus-visible { + outline: 2px solid rgba(31, 83, 181, 0.28); + outline-offset: 2px; +} + +.analytics-drilldown-note { + margin-top: -4px; +} + +body.analytics-drilldown-open { + overflow: hidden; +} + +.analytics-drawer-backdrop { + display: none; + position: fixed; + inset: 0; + z-index: 79; + background: rgba(7, 13, 24, 0.18); +} + +.analytics-drawer { + display: none; + position: fixed; + inset: 0; + z-index: 80; + width: 100%; + height: 100dvh; + padding: 28px 32px; + border: 0; + background: + radial-gradient(circle at top right, rgba(35, 92, 212, 0.08), transparent 34%), + linear-gradient(180deg, #fbfdff, #f1f5fc); + box-shadow: none; + grid-template-rows: auto auto auto minmax(0, 1fr); + gap: 18px; + overflow: hidden; +} + +.analytics-drawer-backdrop.is-open { + display: block; +} + +.analytics-drawer.is-open { + display: grid; +} + +.analytics-drawer-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 0 2px; +} + +.analytics-drawer-close { + white-space: nowrap; +} + +.analytics-drawer-head h2 { + margin: 6px 0 0; + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 34px; + line-height: 1.05; + letter-spacing: -0.03em; +} + +.analytics-drawer-filters { + gap: 8px; + padding: 0 2px; +} + +.analytics-drawer-toolbar { + display: grid; + grid-template-columns: minmax(260px, 1.25fr) repeat(2, minmax(180px, 0.85fr)) auto; + gap: 14px; + padding: 18px 20px; + border-radius: 24px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.94); + box-shadow: 0 18px 36px rgba(31, 83, 181, 0.08); +} + +.analytics-drawer-toolbar .row { + min-width: 0; +} + +.analytics-drawer-toolbar-search input[type="search"] { + width: 100%; +} + +.analytics-drawer-toolbar-actions { + align-self: end; + justify-content: flex-end; +} + +.analytics-drawer-body { + min-height: 0; + display: grid; + grid-template-columns: minmax(360px, 0.86fr) minmax(0, 1.5fr); + gap: 18px; + overflow: hidden; +} + +.analytics-drawer-list-shell, +.analytics-drawer-detail-shell { + min-height: 0; + display: grid; + gap: 12px; + padding: 20px; + border-radius: 24px; + border: 1px solid var(--line); + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 18px 36px rgba(16, 33, 61, 0.06); +} + +.analytics-drawer-list-shell { + grid-template-rows: auto minmax(0, 1fr) auto; +} + +.analytics-drawer-detail-shell { + grid-template-rows: auto minmax(0, 1fr); +} + +.analytics-drawer-list-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + position: sticky; + top: 0; + z-index: 2; + margin: -20px -20px 0; + padding: 20px 20px 12px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.92)); + backdrop-filter: blur(10px); +} + +.analytics-drawer-list-head strong { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 20px; + line-height: 1.1; +} + +.analytics-drawer-list { + min-height: 0; + overflow: auto; + display: grid; + gap: 10px; + scrollbar-gutter: stable; +} + +.analytics-drilldown-row { + width: 100%; + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fbfcff, #f7f9fc); + text-align: left; + cursor: pointer; + font: inherit; + display: grid; + gap: 8px; + transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease; +} + +.analytics-drilldown-row:hover { + transform: translateY(-1px); + border-color: rgba(31, 83, 181, 0.2); + background: linear-gradient(180deg, #ffffff, #f1f6ff); +} + +.analytics-drilldown-row.selected { + border-color: var(--primary); + background: linear-gradient(180deg, #eef4ff, #e5efff); +} + +.analytics-drilldown-row-top, +.analytics-drilldown-row-meta, +.analytics-timeline-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.analytics-drilldown-row-top strong, +.analytics-drilldown-detail-head strong { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 16px; + line-height: 1.2; +} + +.analytics-drilldown-row-meta { + color: var(--text-soft); + font-size: 12px; + flex-wrap: wrap; +} + +.analytics-drilldown-facts { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.analytics-fact-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 5px 9px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + border: 1px solid rgba(16, 33, 61, 0.08); + background: rgba(255, 255, 255, 0.94); + color: var(--text-soft); +} + +.analytics-fact-badge-wait, +.analytics-fact-badge-handle { + background: rgba(31, 83, 181, 0.08); + color: var(--primary-strong); +} + +.analytics-fact-badge-sla, +.analytics-fact-badge-fcr { + background: rgba(24, 160, 99, 0.12); + color: #0d6b42; +} + +.analytics-fact-badge-sla-miss, +.analytics-fact-badge-abandoned { + background: rgba(214, 73, 80, 0.1); + color: #9d313c; +} + +.analytics-drilldown-status, +.analytics-drilldown-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.02em; + border: 1px solid rgba(16, 33, 61, 0.08); +} + +.analytics-drilldown-pill { + background: rgba(244, 247, 252, 0.9); + color: var(--text-soft); +} + +.analytics-drilldown-status { + background: rgba(245, 248, 253, 0.92); + color: var(--text-soft); +} + +.analytics-drilldown-status-new { + background: rgba(39, 124, 255, 0.12); + color: #1b5fc8; +} + +.analytics-drilldown-status-in-progress { + background: rgba(255, 177, 46, 0.18); + color: #9c5b00; +} + +.analytics-drilldown-status-escalated { + background: rgba(223, 80, 68, 0.14); + color: #9b2e25; +} + +.analytics-drilldown-status-closed { + background: rgba(24, 166, 109, 0.16); + color: #16694a; +} + +.analytics-drilldown-status-abandoned { + background: rgba(123, 66, 200, 0.14); + color: #6a34ad; +} + +.analytics-drilldown-status-ready { + background: rgba(24, 166, 109, 0.16); + color: #16694a; +} + +.analytics-drilldown-status-busy { + background: rgba(255, 177, 46, 0.18); + color: #9c5b00; +} + +.analytics-drilldown-status-break { + background: rgba(123, 66, 200, 0.14); + color: #6a34ad; +} + +.analytics-drilldown-status-offline { + background: rgba(16, 33, 61, 0.08); + color: var(--text-soft); +} + +.analytics-drilldown-status-active { + background: rgba(39, 124, 255, 0.12); + color: #1b5fc8; +} + +.analytics-drilldown-status-handoff-required { + background: rgba(223, 80, 68, 0.14); + color: #9b2e25; +} + +.analytics-drilldown-status-human-owned { + background: rgba(255, 177, 46, 0.18); + color: #9c5b00; +} + +.analytics-drilldown-status-error { + background: rgba(123, 66, 200, 0.14); + color: #6a34ad; +} + +.analytics-drawer-pager { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + position: sticky; + bottom: 0; + margin: 0 -20px -20px; + padding: 12px 20px 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.96) 34%); +} + +.analytics-drawer-detail { + min-height: 0; + overflow: auto; + display: grid; + gap: 12px; + scrollbar-gutter: stable; +} + +.analytics-drilldown-detail-card { + padding: 18px; + border-radius: 20px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fbfcff, #f7f9fd); + display: grid; + gap: 14px; + position: sticky; + top: 0; + z-index: 1; +} + +.analytics-drilldown-metric-note { + display: grid; + gap: 8px; + padding: 12px 14px; + border-radius: 16px; + border: 1px solid rgba(31, 83, 181, 0.12); + background: rgba(241, 246, 255, 0.7); +} + +.analytics-drilldown-detail-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.analytics-drilldown-detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.analytics-drilldown-detail-grid div { + padding: 12px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(21, 38, 70, 0.08); + display: grid; + gap: 6px; +} + +.analytics-drilldown-detail-grid small { + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.analytics-drilldown-detail-grid strong { + font-size: 14px; + line-height: 1.45; + word-break: break-word; +} + +.analytics-drilldown-timeline { + display: grid; + gap: 10px; + padding-bottom: 8px; +} + +.analytics-timeline-item { + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line); + background: #ffffff; + display: grid; + gap: 8px; +} + +.analytics-timeline-meta span { + color: var(--text-soft); + font-size: 12px; +} + +.analytics-timeline-item pre { + margin: 0; + padding: 12px; + border-radius: 14px; + background: #f4f7fb; + color: var(--text-soft); + font-size: 12px; + line-height: 1.5; + overflow: auto; +} + +.analytics-coverage { + display: grid; + gap: 14px; +} + +.analytics-coverage-list { + display: grid; + gap: 12px; +} + +.analytics-coverage-block h4 { + margin: 0 0 8px; + color: var(--text-faint); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.analytics-coverage-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.analytics-coverage-chip { + display: inline-flex; + align-items: center; + padding: 8px 10px; + border-radius: 999px; + background: var(--primary-soft); + border: 1px solid var(--primary-line); + color: var(--primary); + font-size: 12px; + font-weight: 700; +} + +.analytics-coverage-chip.soft { + background: #f5f7fb; + border-color: var(--line); + color: var(--text-soft); +} + +.analytics-placeholder-grid { + display: grid; + gap: 10px; +} + +.analytics-placeholder-card { + padding: 14px; + border-radius: 16px; + border: 1px dashed var(--line-strong); + background: linear-gradient(180deg, #fbfcff, #f7f9fd); +} + +.analytics-placeholder-card p { + margin: 6px 0 0; + color: var(--text-soft); + font-size: 13px; + line-height: 1.5; +} + +.agent-analytics-table-foot { + padding: 12px 14px; + border-top: 1px solid var(--line); + color: var(--text-faint); + font-size: 12px; +} + +.softphone-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 12px; +} + +.softphone-summary.compact { + margin-bottom: 14px; +} + +.softphone-status-block { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #ffffff, #f8fbff); +} + +.softphone-status-label { + font-size: 11px; + font-weight: 800; + color: var(--text-faint); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.softphone-status-value { + margin-top: 8px; + font-size: 16px; + font-weight: 700; + color: var(--text); + word-break: break-word; +} + +.row.stacked { + display: grid; + gap: 6px; +} + +.softphone-banner { + margin-bottom: 12px; + padding: 16px; + border-radius: 18px; + border: 1px solid var(--primary-line); + background: linear-gradient(180deg, #f7f9ff, #edf2ff); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.softphone-banner p { + margin: 6px 0 0; + color: var(--text-soft); +} + +.softphone-banner-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.live-call-inspector { + grid-template-columns: 1fr; +} + +.call-window-overlay { + position: fixed; + inset: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: rgba(12, 21, 35, 0.45); +} + +.call-window { + width: min(620px, 100%); + padding: 22px; + border-radius: 28px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: linear-gradient(180deg, rgba(17, 25, 41, 0.97), rgba(8, 13, 22, 0.97)); + color: #f5f7fb; + box-shadow: 0 28px 60px rgba(8, 13, 22, 0.4); +} + +.call-window-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.call-window-status { + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #95b7ff; +} + +.call-window-title { + margin: 8px 0 4px; + font-size: 34px; + line-height: 1.05; + letter-spacing: -0.04em; +} + +.call-window-meta { + margin: 0; + color: rgba(245, 247, 251, 0.9); + font-size: 15px; +} + +.call-window-meta.subtle { + margin-top: 6px; + color: rgba(200, 210, 224, 0.78); + font-size: 13px; +} + +.call-window-body { + display: grid; + justify-items: center; + gap: 16px; + padding: 28px 0 22px; +} + +.call-window-avatar { + width: 112px; + height: 112px; + border-radius: 50%; + display: grid; + place-items: center; + background: radial-gradient(circle at top, rgba(95, 133, 255, 0.75), rgba(40, 63, 112, 0.95)); + border: 1px solid rgba(255, 255, 255, 0.12); + font-size: 42px; +} + +.call-window-timer { + font-family: "Manrope", "IBM Plex Sans", sans-serif; + font-size: 24px; + font-weight: 800; + letter-spacing: -0.03em; +} + +.call-window-warning { + margin: 0; + width: min(420px, 100%); + padding: 10px 12px; + border-radius: 14px; + background: rgba(239, 68, 68, 0.16); + border: 1px solid rgba(239, 68, 68, 0.32); + color: #ffd0d0; + text-align: center; + font-size: 13px; + line-height: 1.45; +} + +.call-window-ai-summary { + margin-bottom: 14px; +} + +.call-window-name-editor { + margin-bottom: 14px; + padding: 14px 16px; + border-radius: 18px; + background: rgba(9, 16, 28, 0.42); + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; + gap: 10px; +} + +.call-window-name-head { + display: grid; + gap: 4px; +} + +.call-window-name-form { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 10px; +} + +.call-window-name-form input { + min-width: 0; + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + color: #fff; +} + +.call-window-name-form input::placeholder { + color: rgba(255, 255, 255, 0.45); +} + +.voice-ai-summary { + padding: 14px 16px; + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + display: grid; + gap: 12px; +} + +.voice-ai-summary-loading { + opacity: 0.92; +} + +.voice-ai-summary-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.voice-ai-summary-title { + color: rgba(229, 236, 248, 0.92); + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.voice-ai-summary-caption, +.voice-ai-summary-meta { + color: rgba(209, 220, 236, 0.72); + font-size: 12px; + line-height: 1.45; +} + +.voice-ai-summary-side { + display: grid; + justify-items: end; + gap: 8px; +} + +.voice-ai-summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.voice-summary-field { + padding: 12px 14px; + border-radius: 16px; + background: rgba(9, 16, 28, 0.46); + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; + gap: 6px; +} + +.voice-summary-label { + color: rgba(175, 189, 210, 0.76); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.voice-summary-value { + color: #f5f7fb; + font-size: 14px; + line-height: 1.5; + white-space: pre-wrap; +} + +.voice-summary-status { + min-height: 28px; + padding: 0 12px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.voice-summary-status.answered { + background: rgba(16, 185, 129, 0.16); + color: #70e0bb; +} + +.voice-summary-status.handoff { + background: rgba(245, 158, 11, 0.18); + color: #f3bf6a; +} + +.voice-summary-transcript { + display: grid; + gap: 10px; +} + +.voice-summary-transcript-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.voice-summary-transcript-meta { + color: rgba(175, 189, 210, 0.72); + font-size: 11px; +} + +.voice-summary-transcript-list { + display: grid; + gap: 8px; + max-height: 240px; + overflow-y: auto; + padding-right: 4px; +} + +.voice-transcript-line { + padding: 10px 12px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + display: grid; + gap: 6px; +} + +.voice-transcript-line.caller { + background: rgba(9, 16, 28, 0.42); +} + +.voice-transcript-line.assistant { + background: rgba(56, 96, 201, 0.18); +} + +.voice-transcript-speaker-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.voice-transcript-speaker { + color: #d7e2f3; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.voice-transcript-time { + color: rgba(175, 189, 210, 0.68); + font-size: 11px; +} + +.voice-transcript-text { + color: #f5f7fb; + font-size: 14px; + line-height: 1.5; + white-space: pre-wrap; +} + +.call-window-transfer { + display: grid; + grid-template-columns: 160px 1fr; + gap: 10px; + margin-bottom: 14px; +} + +.call-window-transfer select, +.call-window-transfer input { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + color: #fff; +} + +.call-window-transfer input::placeholder { + color: rgba(255, 255, 255, 0.45); +} + +.call-window-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.call-window .icon-button { + border-color: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.06); + color: #fff; +} + +.hidden { + display: none !important; +} + +#browserPhoneRemoteAudio { + width: 100%; + margin-top: 10px; +} + +.output { + margin: 0; + padding: 14px; + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: #f8fbff; + color: var(--text-soft); + white-space: pre-wrap; + max-height: 220px; + overflow: auto; + font-size: 12px; + line-height: 1.55; +} + +.output.tall { + max-height: 340px; +} + +.compact-actions { + margin-bottom: 12px; +} + +.technical-panel { + background: linear-gradient(180deg, #ffffff, #fbfcff); +} + +.technical-details { + margin-top: 8px; + padding: 14px 16px 10px; + border-radius: 18px; + border: 1px solid var(--line); + background: #f9fbff; +} + +.technical-details > summary { + cursor: pointer; + color: var(--text); + font-size: 15px; + font-weight: 800; + list-style: none; +} + +.technical-details > summary::-webkit-details-marker { + display: none; +} + +.technical-details[open] { + background: #fff; +} + +.details-block { + margin-bottom: 12px; +} + +.details-block summary { + cursor: pointer; + color: var(--text-soft); + font-size: 13px; + font-weight: 700; + margin-bottom: 8px; +} + +.empty-state { + color: var(--text-faint); + font-style: italic; +} + +.reveal { + animation: rise 0.45s ease both; +} + +.reveal:nth-child(2) { animation-delay: 0.04s; } +.reveal:nth-child(3) { animation-delay: 0.08s; } +.reveal:nth-child(4) { animation-delay: 0.12s; } + +@keyframes rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 1200px) { + .app-shell { + display: block; + } + + .sidebar { + width: auto; + position: static; + min-height: auto; + border-right: 0; + border-bottom: 1px solid var(--line); + overflow-y: visible; + } + + .workspace { + margin-left: 0; + } +} + +@media (max-width: 980px) { + .workspace-topbar, + .layout { + padding-left: 18px; + padding-right: 18px; + } + + .workspace-topbar { + height: auto; + align-items: stretch; + flex-direction: column; + } + + .login-grid { + grid-template-columns: 1fr; + } + + .search-box { + max-width: none; + } + + .topbar-actions, + .guide-head { + flex-direction: column; + align-items: stretch; + } + + .auth, + .grid, + .inline-form, + .inline-form.compact, + .inline-form.split-two, + .summary-grid, + .softphone-summary, + .analytics-filter-bar, + .analytics-toolkit, + .analytics-toolkit-group-wide, + .analytics-compare-grid, + .analytics-overview-grid, + .analytics-sections { + grid-template-columns: 1fr; + } + + .ai-analytics-coverage-grid, + .ai-analytics-channel-grid, + .ai-analytics-outcome-grid, + .ai-analytics-reason-grid { + grid-template-columns: 1fr; + } + + .analytics-head, + .analytics-compare-head, + .analytics-subhead, + .analytics-chart-meta, + .analytics-narrative-meta, + .unified-inbox-head { + flex-direction: column; + align-items: stretch; + } + + .analytics-table-shell { + overflow: auto; + } + + .analytics-table-head, + .analytics-table-row { + min-width: 620px; + } + + .pipeline-board { + grid-template-columns: 1fr; + } + + .unified-inbox-board { + min-width: 0; + } + + .customer-spotlight-grid, + .customer-spotlight-grid.page, + .customer-profile-summary-grid, + .customer-profile-meta-grid { + grid-template-columns: 1fr; + } + + .customer-profile-hero, + .customer-history-header { + flex-direction: column; + } + + .customer-profile-name-row { + flex-direction: column; + align-items: stretch; + } + + .customer-profile-badges { + justify-content: flex-start; + max-width: none; + } + + .customer-history-caption { + max-width: none; + text-align: left; + } + + .customers-toolbar, + .customers-footer { + align-items: stretch; + } + + .customers-toolbar-left, + .customers-toolbar-right, + .customers-search { + width: 100%; + max-width: none; + } + + .lead-table-head, + .lead-table-row { + grid-template-columns: 44px minmax(200px, 2fr) minmax(120px, 1fr) minmax(120px, 1fr) minmax(140px, 1fr) 90px; + } + + .call-window { + padding: 18px; + } + + .call-window-transfer { + grid-template-columns: 1fr; + } + + .call-window-name-form, + .live-call-name-editor-form, + .customer-profile-name-editor-form { + grid-template-columns: 1fr; + } + + .call-window-actions { + flex-direction: column; + } + + .voice-ai-summary-head { + flex-direction: column; + } + + .voice-ai-summary-side { + justify-items: start; + } + + .voice-ai-summary-grid { + grid-template-columns: 1fr; + } + + .profile-chip { + border-left: 0; + padding-left: 0; + } + + .messenger-layout, + .telegram-layout { + grid-template-columns: 1fr; + } + + .messenger-list-pane, + .messenger-context-pane { + border-left: 0; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .messenger-list-pane { + min-height: 320px; + } + + .messenger-context-pane { + order: 3; + } + + .messenger-chat-header, + .messenger-chat-actions { + flex-wrap: wrap; + } + + .messenger-chat-surface { + min-height: 420px; + } + + .telegram-threads-pane { + border-right: 0; + border-bottom: 1px solid var(--line); + min-height: 320px; + } + + .telegram-chat-header, + .telegram-operator-controls { + flex-wrap: wrap; + } + + .telegram-ai-summary-head { + flex-direction: column; + } + + .telegram-ai-summary-meta { + white-space: normal; + text-align: left; + } + + .telegram-ai-summary-side { + justify-items: start; + } + + .telegram-ai-summary-grid { + grid-template-columns: 1fr; + } + + .telegram-chat-surface { + min-height: 420px; + } + + .telegram-composer-shell { + grid-template-columns: 40px minmax(0, 1fr) 40px 44px; + } + + .whatsapp-shell { + grid-template-columns: 1fr; + grid-template-rows: auto auto minmax(520px, 1fr); + } + + .whatsapp-nav-rail { + padding: 10px 14px; + border-right: 0; + border-bottom: 1px solid rgba(233, 237, 239, 0.08); + flex-direction: row; + } + + .whatsapp-rail-group, + .whatsapp-rail-footer { + flex-direction: row; + } + + .whatsapp-list-pane { + border-right: 0; + border-bottom: 1px solid rgba(233, 237, 239, 0.08); + min-height: 320px; + } + + .whatsapp-chat-head { + flex-wrap: wrap; + } + + .whatsapp-context-panel { + padding-left: 12px; + padding-right: 12px; + } + + .whatsapp-ai-summary-head { + flex-direction: column; + } + + .whatsapp-ai-summary-side { + justify-items: start; + } + + .whatsapp-ai-summary-grid { + grid-template-columns: 1fr; + } + + .whatsapp-chat-surface { + min-height: 420px; + } +} + +@media (max-width: 880px) { + .lead-table-head { + display: none; + } + + .lead-table-row { + grid-template-columns: 1fr; + gap: 10px; + padding: 16px; + } + + .customer-spotlight-shell { + padding: 14px; + } + + .customer-spotlight-page { + padding: 16px; + } + + .customer-profile-card, + .customer-history-card { + padding: 16px; + border-radius: 20px; + } + + .customer-profile-copy h3, + .customer-history-header h3 { + font-size: 24px; + } + + .customer-history-event { + grid-template-columns: 1fr; + } + + .customer-history-mark { + width: 40px; + height: 40px; + border-radius: 14px; + } + + .telegram-thread-card { + padding: 10px; + } + + .telegram-thread-avatar { + width: 38px; + height: 38px; + font-size: 11px; + } + + .telegram-thread-title { + font-size: 18px; + } + + .telegram-message-bubble { + max-width: 92%; + } + + .telegram-composer-wrap { + padding-left: 10px; + padding-right: 10px; + } + + .telegram-composer-shell { + grid-template-columns: 38px minmax(0, 1fr) 38px 42px; + } + + .whatsapp-list-head, + .whatsapp-chat-head, + .whatsapp-context-panel, + .whatsapp-search-wrap, + .whatsapp-composer-wrap { + padding-left: 12px; + padding-right: 12px; + } + + .whatsapp-chat-card { + padding: 12px; + } + + .whatsapp-chat-card-avatar { + width: 42px; + height: 42px; + font-size: 13px; + } + + .whatsapp-chat-card-title { + font-size: 14px; + } + + .whatsapp-message-timeline { + padding-left: 10px; + padding-right: 10px; + } + + .whatsapp-message-bubble { + max-width: 94%; + } + + .whatsapp-composer-shell { + grid-template-columns: 38px minmax(0, 1fr) 38px 42px; + gap: 6px; + } + + .checkbox-cell { + display: none; + } + + .score-cell { + text-align: left; + } +} + +@media (max-width: 960px) { + .analytics-drawer { + padding: 22px; + } + + .analytics-drawer-body { + grid-template-columns: 1fr; + grid-template-rows: minmax(240px, 36dvh) minmax(0, 1fr); + } + + .analytics-drawer-toolbar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .analytics-drawer-toolbar-actions { + grid-column: 1 / -1; + justify-content: stretch; + } + + .analytics-drawer-list-shell, + .analytics-drawer-detail-shell { + min-height: 0; + } +} + +@media (max-width: 720px) { + .analytics-drawer { + inset: 0; + height: 100dvh; + border-radius: 0; + padding: 14px; + gap: 14px; + } + + .analytics-drawer-head { + flex-direction: column; + align-items: stretch; + } + + .analytics-drawer-head h2 { + font-size: 28px; + } + + .analytics-drawer-toolbar { + grid-template-columns: 1fr; + padding: 14px; + gap: 12px; + } + + .analytics-drawer-toolbar-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + justify-content: stretch; + } + + .analytics-drawer-body { + grid-template-rows: minmax(220px, 34dvh) minmax(0, 1fr); + gap: 12px; + } + + .analytics-drilldown-detail-grid { + grid-template-columns: 1fr; + } + + .analytics-drawer-list-head { + margin: -16px -16px 0; + padding: 16px 16px 10px; + } + + .analytics-drawer-pager { + margin: 0 -16px -16px; + padding: 10px 16px 0; + } +} diff --git a/ui/operator/vendor/sip-0.21.2.min.js b/ui/operator/vendor/sip-0.21.2.min.js new file mode 100644 index 0000000..5b917b4 --- /dev/null +++ b/ui/operator/vendor/sip-0.21.2.min.js @@ -0,0 +1,2 @@ +/*! For license information please see sip.min.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SIP=t():e.SIP=t()}(this,(()=>(()=>{"use strict";var e={d:(t,s)=>{for(var i in s)e.o(s,i)&&!e.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:s[i]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{Ack:()=>l,Bye:()=>g,Cancel:()=>u,ContentTypeUnsupportedError:()=>o,Core:()=>s,EmitterImpl:()=>p,Grammar:()=>y,Info:()=>f,Invitation:()=>Q,Inviter:()=>ee,Message:()=>W,Messager:()=>te,NameAddrHeader:()=>v,Notification:()=>K,Parameters:()=>m,Publisher:()=>le,PublisherState:()=>se,Referral:()=>Y,Registerer:()=>ge,RegistererState:()=>ie,RequestPendingError:()=>a,SIPExtension:()=>J,Session:()=>z,SessionDescriptionHandlerError:()=>c,SessionState:()=>Z,SessionTerminatedError:()=>h,StateTransitionError:()=>d,Subscriber:()=>pe,Subscription:()=>ue,SubscriptionState:()=>ne,TransportState:()=>oe,URI:()=>w,UserAgent:()=>dt,UserAgentRegisteredOptionTags:()=>X,UserAgentState:()=>ae,Web:()=>i,equivalentURI:()=>b,name:()=>At,version:()=>Ct});var s={};e.r(s),e.d(s,{ByeUserAgentClient:()=>Pe,ByeUserAgentServer:()=>_e,C:()=>G,CancelUserAgentClient:()=>lt,ClientTransaction:()=>Ie,Dialog:()=>Ae,DigestAuthentication:()=>we,Exception:()=>n,Grammar:()=>y,IncomingMessage:()=>H,IncomingRequestMessage:()=>P,IncomingResponseMessage:()=>x,InfoUserAgentClient:()=>qe,InfoUserAgentServer:()=>Me,InviteClientTransaction:()=>De,InviteServerTransaction:()=>$e,InviteUserAgentClient:()=>Ye,InviteUserAgentServer:()=>Ze,Levels:()=>ce,Logger:()=>be,LoggerFactory:()=>Te,MessageUserAgentClient:()=>Ne,MessageUserAgentServer:()=>Oe,NameAddrHeader:()=>v,NonInviteClientTransaction:()=>Ce,NonInviteServerTransaction:()=>xe,NotifyUserAgentClient:()=>Ue,NotifyUserAgentServer:()=>je,OutgoingRequestMessage:()=>k,Parameters:()=>m,Parser:()=>he,PrackUserAgentClient:()=>Fe,PrackUserAgentServer:()=>Le,PublishUserAgentClient:()=>Je,ReInviteUserAgentClient:()=>Be,ReInviteUserAgentServer:()=>Ge,ReSubscribeUserAgentClient:()=>Qe,ReSubscribeUserAgentServer:()=>gt,ReferUserAgentClient:()=>Ve,ReferUserAgentServer:()=>We,RegisterUserAgentClient:()=>ze,RegisterUserAgentServer:()=>Xe,ServerTransaction:()=>Ee,SessionDialog:()=>Ke,SessionState:()=>O,SignalingState:()=>U,SubscribeUserAgentClient:()=>tt,SubscribeUserAgentServer:()=>st,SubscriptionDialog:()=>et,SubscriptionState:()=>re,Timers:()=>L,Transaction:()=>ye,TransactionState:()=>de,TransactionStateError:()=>B,TransportError:()=>Re,URI:()=>w,UserAgentClient:()=>He,UserAgentCore:()=>rt,UserAgentServer:()=>ke,constructOutgoingResponse:()=>Se,equivalentURI:()=>b,fromBodyLegacy:()=>q,getBody:()=>N,isBody:()=>M});var i={};e.r(i),e.d(i,{SessionDescriptionHandler:()=>at,SessionManager:()=>$t,SimpleUser:()=>It,Transport:()=>ht,WebAudioSessionDescriptionHandler:()=>yt,addMidLines:()=>Tt,cleanJitsiSdpImageattr:()=>mt,defaultManagedSessionFactory:()=>Et,defaultMediaStreamFactory:()=>nt,defaultPeerConnectionConfiguration:()=>ot,defaultSessionDescriptionHandlerFactory:()=>ct,holdModifier:()=>St,startLocalConference:()=>Rt,stripG722:()=>vt,stripRtpPayload:()=>wt,stripTcpCandidates:()=>pt,stripTelephoneEvent:()=>ft,stripVideo:()=>bt});const r="0.21.2";class n extends Error{constructor(e){super(e),Object.setPrototypeOf(this,new.target.prototype)}}class o extends n{constructor(e){super(e||"Unsupported content type.")}}class a extends n{constructor(e){super(e||"Request pending.")}}class c extends n{constructor(e){super(e||"Unspecified session description handler error.")}}class h extends n{constructor(){super("The session has terminated.")}}class d extends n{constructor(e){super(e||"An error occurred during state transition.")}}class l{constructor(e){this.incomingAckRequest=e}get request(){return this.incomingAckRequest.message}}class g{constructor(e){this.incomingByeRequest=e}get request(){return this.incomingByeRequest.message}accept(e){return this.incomingByeRequest.accept(e),Promise.resolve()}reject(e){return this.incomingByeRequest.reject(e),Promise.resolve()}}class u{constructor(e){this.incomingCancelRequest=e}get request(){return this.incomingCancelRequest}}class p{constructor(){this.listeners=new Array}addListener(e,t){const s=t=>{this.removeListener(s),e(t)};!0===(null==t?void 0:t.once)?this.listeners.push(s):this.listeners.push(e)}emit(e){this.listeners.slice().forEach((t=>t(e)))}removeAllListeners(){this.listeners=[]}removeListener(e){this.listeners=this.listeners.filter((t=>t!==e))}on(e){return this.addListener(e)}off(e){return this.removeListener(e)}once(e){return this.addListener(e,{once:!0})}}class f{constructor(e){this.incomingInfoRequest=e}get request(){return this.incomingInfoRequest.message}accept(e){return this.incomingInfoRequest.accept(e),Promise.resolve()}reject(e){return this.incomingInfoRequest.reject(e),Promise.resolve()}}class m{constructor(e){this.parameters={};for(const t in e)e.hasOwnProperty(t)&&this.setParam(t,e[t])}setParam(e,t){e&&(this.parameters[e.toLowerCase()]=null==t?null:t.toString())}getParam(e){if(e)return this.parameters[e.toLowerCase()]}hasParam(e){return!(!e||void 0===this.parameters[e.toLowerCase()])}deleteParam(e){if(e=e.toLowerCase(),this.hasParam(e)){const t=this.parameters[e];return delete this.parameters[e],t}}clearParams(){this.parameters={}}}class v extends m{constructor(e,t,s){super(s),this.uri=e,this._displayName=t}get friendlyName(){return this.displayName||this.uri.aor}get displayName(){return this._displayName}set displayName(e){this._displayName=e}clone(){return new v(this.uri.clone(),this._displayName,JSON.parse(JSON.stringify(this.parameters)))}toString(){let e=this.displayName||"0"===this.displayName?'"'+this.displayName+'" ':"";e+="<"+this.uri.toString()+">";for(const t in this.parameters)this.parameters.hasOwnProperty(t)&&(e+=";"+t,null!==this.parameters[t]&&(e+="="+this.parameters[t]));return e}}class w extends m{constructor(e="sip",t,s,i,r,n){if(super(r||{}),this.headers={},!s)throw new TypeError('missing or invalid "host" parameter');for(const e in n)n.hasOwnProperty(e)&&this.setHeader(e,n[e]);this.raw={scheme:e,user:t,host:s,port:i},this.normal={scheme:e.toLowerCase(),user:t,host:s.toLowerCase(),port:i}}get scheme(){return this.normal.scheme}set scheme(e){this.raw.scheme=e,this.normal.scheme=e.toLowerCase()}get user(){return this.normal.user}set user(e){this.normal.user=this.raw.user=e}get host(){return this.normal.host}set host(e){this.raw.host=e,this.normal.host=e.toLowerCase()}get aor(){return this.normal.user+"@"+this.normal.host}get port(){return this.normal.port}set port(e){this.normal.port=this.raw.port=e}setHeader(e,t){this.headers[this.headerize(e)]=t instanceof Array?t:[t]}getHeader(e){if(e)return this.headers[this.headerize(e)]}hasHeader(e){return!!e&&!!this.headers.hasOwnProperty(this.headerize(e))}deleteHeader(e){if(e=this.headerize(e),this.headers.hasOwnProperty(e)){const t=this.headers[e];return delete this.headers[e],t}}clearHeaders(){this.headers={}}clone(){return new w(this._raw.scheme,this._raw.user||"",this._raw.host,this._raw.port,JSON.parse(JSON.stringify(this.parameters)),JSON.parse(JSON.stringify(this.headers)))}toRaw(){return this._toString(this._raw)}toString(){return this._toString(this._normal)}get _normal(){return this.normal}get _raw(){return this.raw}_toString(e){let t=e.scheme+":";e.scheme.toLowerCase().match("^sips?$")||(t+="//"),e.user&&(t+=this.escapeUser(e.user)+"@"),t+=e.host,(e.port||0===e.port)&&(t+=":"+e.port);for(const e in this.parameters)this.parameters.hasOwnProperty(e)&&(t+=";"+e,null!==this.parameters[e]&&(t+="="+this.parameters[e]));const s=[];for(const e in this.headers)if(this.headers.hasOwnProperty(e))for(const t in this.headers[e])this.headers[e].hasOwnProperty(t)&&s.push(e+"="+this.headers[e][t]);return s.length>0&&(t+="?"+s.join("&")),t}escapeUser(e){let t;try{t=decodeURIComponent(e)}catch(e){throw e}return encodeURIComponent(t).replace(/%3A/gi,":").replace(/%2B/gi,"+").replace(/%3F/gi,"?").replace(/%2F/gi,"/")}headerize(e){const t={"Call-Id":"Call-ID",Cseq:"CSeq","Min-Se":"Min-SE",Rack:"RAck",Rseq:"RSeq","Www-Authenticate":"WWW-Authenticate"},s=e.toLowerCase().replace(/_/g,"-").split("-"),i=s.length;let r="";for(let e=0;ei.includes(e))).every((s=>e.parameters[s]===t.parameters[s]))&&(!!["user","ttl","method","transport"].every((s=>e.hasParam(s)&&t.hasParam(s)||!e.hasParam(s)&&!t.hasParam(s)))&&!!["maddr"].every((s=>e.hasParam(s)&&t.hasParam(s)||!e.hasParam(s)&&!t.hasParam(s))))}(e,t))return!1;const s=Object.keys(e.headers),i=Object.keys(t.headers);if(0!==s.length||0!==i.length){if(s.length!==i.length)return!1;const r=s.filter((e=>i.includes(e)));if(r.length!==i.length)return!1;if(!r.every((s=>e.headers[s].length&&t.headers[s].length&&e.headers[s][0]===t.headers[s][0])))return!1}return!0}function T(e,t,s){return s=s||" ",e.length>t?e:(t-=e.length,e+(s+=s.repeat(t)).slice(0,t))}class S extends Error{constructor(e,t,s,i){super(),this.message=e,this.expected=t,this.found=s,this.location=i,this.name="SyntaxError","function"==typeof Object.setPrototypeOf?Object.setPrototypeOf(this,S.prototype):this.__proto__=S.prototype,"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,S)}static buildMessage(e,t){function s(e){return e.charCodeAt(0).toString(16).toUpperCase()}function i(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\0/g,"\\0").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/[\x00-\x0F]/g,(e=>"\\x0"+s(e))).replace(/[\x10-\x1F\x7F-\x9F]/g,(e=>"\\x"+s(e)))}function r(e){return e.replace(/\\/g,"\\\\").replace(/\]/g,"\\]").replace(/\^/g,"\\^").replace(/-/g,"\\-").replace(/\0/g,"\\0").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/[\x00-\x0F]/g,(e=>"\\x0"+s(e))).replace(/[\x10-\x1F\x7F-\x9F]/g,(e=>"\\x"+s(e)))}function n(e){switch(e.type){case"literal":return'"'+i(e.text)+'"';case"class":const t=e.parts.map((e=>Array.isArray(e)?r(e[0])+"-"+r(e[1]):r(e)));return"["+(e.inverted?"^":"")+t+"]";case"any":return"any character";case"end":return"end of input";case"other":return e.description}}return"Expected "+function(e){const t=e.map(n);let s,i;if(t.sort(),t.length>0){for(s=1,i=1;s",b(">",!1),"\\",b("\\",!1),"[",b("[",!1),"]",b("]",!1),"{",b("{",!1),"}",b("}",!1),function(){return"*"},function(){return"/"},function(){return"="},function(){return"("},function(){return")"},function(){return">"},function(){return"<"},function(){return","},function(){return";"},function(){return":"},function(){return'"'},/^[!-']/,T([["!","'"]],!1,!1),/^[*-[]/,T([["*","["]],!1,!1),/^[\]-~]/,T([["]","~"]],!1,!1),function(e){return e},/^[#-[]/,T([["#","["]],!1,!1),/^[\0-\t]/,T([["\0","\t"]],!1,!1),/^[\v-\f]/,T([["\v","\f"]],!1,!1),/^[\x0E-\x7F]/,T([["\x0e","\x7f"]],!1,!1),function(){(t=t||{data:{}}).data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port},function(){(t=t||{data:{}}).data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port,t.data.uri_params,t.data.uri_headers),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port,delete t.data.uri_params,"SIP_URI"===t.startRule&&(t.data=t.data.uri)},"sips",b("sips",!0),"sip",b("sip",!0),function(e){(t=t||{data:{}}).data.scheme=e},function(){(t=t||{data:{}}).data.user=decodeURIComponent(f().slice(0,-1))},function(){(t=t||{data:{}}).data.password=f()},function(){return(t=t||{data:{}}).data.host=f(),t.data.host},function(){return(t=t||{data:{}}).data.host_type="domain",f()},/^[a-zA-Z0-9_\-]/,T([["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),/^[a-zA-Z0-9\-]/,T([["a","z"],["A","Z"],["0","9"],"-"],!1,!1),function(){return(t=t||{data:{}}).data.host_type="IPv6",f()},"::",b("::",!1),function(){return(t=t||{data:{}}).data.host_type="IPv6",f()},function(){return(t=t||{data:{}}).data.host_type="IPv4",f()},"25",b("25",!1),/^[0-5]/,T([["0","5"]],!1,!1),"2",b("2",!1),/^[0-4]/,T([["0","4"]],!1,!1),"1",b("1",!1),/^[1-9]/,T([["1","9"]],!1,!1),function(e){return t=t||{data:{}},e=parseInt(e.join("")),t.data.port=e,e},"transport=",b("transport=",!0),"udp",b("udp",!0),"tcp",b("tcp",!0),"sctp",b("sctp",!0),"tls",b("tls",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.transport=e.toLowerCase()},"user=",b("user=",!0),"phone",b("phone",!0),"ip",b("ip",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.user=e.toLowerCase()},"method=",b("method=",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.method=e},"ttl=",b("ttl=",!0),function(e){(t=t||{data:{}}).data.params||(t.data.params={}),t.data.params.ttl=e},"maddr=",b("maddr=",!0),function(e){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.maddr=e},"lr",b("lr",!0),function(){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),t.data.uri_params.lr=void 0},function(e,s){(t=t||{data:{}}).data.uri_params||(t.data.uri_params={}),s=null===s?void 0:s[1],t.data.uri_params[e.toLowerCase()]=s},function(e,s){e=e.join("").toLowerCase(),s=s.join(""),(t=t||{data:{}}).data.uri_headers||(t.data.uri_headers={}),t.data.uri_headers[e]?t.data.uri_headers[e].push(s):t.data.uri_headers[e]=[s]},function(){"Refer_To"===(t=t||{data:{}}).startRule&&(t.data.uri=new w(t.data.scheme,t.data.user,t.data.host,t.data.port,t.data.uri_params,t.data.uri_headers),delete t.data.scheme,delete t.data.user,delete t.data.host,delete t.data.host_type,delete t.data.port,delete t.data.uri_params)},"//",b("//",!1),function(){(t=t||{data:{}}).data.scheme=f()},b("SIP",!0),function(){(t=t||{data:{}}).data.sip_version=f()},"INVITE",b("INVITE",!1),"ACK",b("ACK",!1),"VXACH",b("VXACH",!1),"OPTIONS",b("OPTIONS",!1),"BYE",b("BYE",!1),"CANCEL",b("CANCEL",!1),"REGISTER",b("REGISTER",!1),"SUBSCRIBE",b("SUBSCRIBE",!1),"NOTIFY",b("NOTIFY",!1),"REFER",b("REFER",!1),"PUBLISH",b("PUBLISH",!1),function(){return(t=t||{data:{}}).data.method=f(),t.data.method},function(e){(t=t||{data:{}}).data.status_code=parseInt(e.join(""))},function(){(t=t||{data:{}}).data.reason_phrase=f()},function(){(t=t||{data:{}}).data=f()},function(){var e,s;for(s=(t=t||{data:{}}).data.multi_header.length,e=0;e""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I(";).# &;,"),I('2F""6F7G.} &2H""6H7I.q &2J""6J7K.e &2L""6L7M.Y &2N""6N7O.M &2P""6P7Q.A &2R""6R7S.5 &2T""6T7U.) &2V""6V7W'),I('%%2X""6X7Y/5#;#/,$;#/#$+#)(#\'#("\'#&\'#/"!&,)'),I('%%$;$0#*;$&/,#; /#$+")("\'#&\'#." &"/=#$;$/�#*;$&&&#/\'$8":Z" )("\'#&\'#'),I(';.." &"'),I("%$;'.# &;(0)*;'.# &;(&/?#28\"\"6879/0$;//'$8#:[# )(#'#(\"'#&'#"),I('%%$;2/�#*;2&&&#/g#$%$;.0#*;.&/,#;2/#$+")("\'#&\'#0=*%$;.0#*;.&/,#;2/#$+")("\'#&\'#&/#$+")("\'#&\'#/"!&,)'),I('4\\""5!7].# &;3'),I('4^""5!7_'),I('4`""5!7a'),I(';!.) &4b""5!7c'),I('%$;).\x95 &2F""6F7G.\x89 &2J""6J7K.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O/\x9e#0\x9b*;).\x95 &2F""6F7G.\x89 &2J""6J7K.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O&&&#/"!&,)'),I('%$;).\x89 &2F""6F7G.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O/\x92#0\x8f*;).\x89 &2F""6F7G.} &2L""6L7M.q &2X""6X7Y.e &2P""6P7Q.Y &2H""6H7I.M &2@""6@7A.A &2d""6d7e.5 &2R""6R7S.) &2N""6N7O&&&#/"!&,)'),I('2T""6T7U.\xe3 &2V""6V7W.\xd7 &2f""6f7g.\xcb &2h""6h7i.\xbf &2:""6:7;.\xb3 &2D""6D7E.\xa7 &22""6273.\x9b &28""6879.\x8f &2j""6j7k.\x83 &;&.} &24""6475.q &2l""6l7m.e &2n""6n7o.Y &26""6677.M &2>""6>7?.A &2p""6p7q.5 &2r""6r7s.) &;\'.# &;('),I('%$;).\u012b &2F""6F7G.\u011f &2J""6J7K.\u0113 &2L""6L7M.\u0107 &2X""6X7Y.\xfb &2P""6P7Q.\xef &2H""6H7I.\xe3 &2@""6@7A.\xd7 &2d""6d7e.\xcb &2R""6R7S.\xbf &2N""6N7O.\xb3 &2T""6T7U.\xa7 &2V""6V7W.\x9b &2f""6f7g.\x8f &2h""6h7i.\x83 &28""6879.w &2j""6j7k.k &;&.e &24""6475.Y &2l""6l7m.M &2n""6n7o.A &26""6677.5 &2p""6p7q.) &2r""6r7s/\u0134#0\u0131*;).\u012b &2F""6F7G.\u011f &2J""6J7K.\u0113 &2L""6L7M.\u0107 &2X""6X7Y.\xfb &2P""6P7Q.\xef &2H""6H7I.\xe3 &2@""6@7A.\xd7 &2d""6d7e.\xcb &2R""6R7S.\xbf &2N""6N7O.\xb3 &2T""6T7U.\xa7 &2V""6V7W.\x9b &2f""6f7g.\x8f &2h""6h7i.\x83 &28""6879.w &2j""6j7k.k &;&.e &24""6475.Y &2l""6l7m.M &2n""6n7o.A &26""6677.5 &2p""6p7q.) &2r""6r7s&&&#/"!&,)'),I("%;//?#2P\"\"6P7Q/0$;//'$8#:t# )(#'#(\"'#&'#"),I("%;//?#24\"\"6475/0$;//'$8#:u# )(#'#(\"'#&'#"),I("%;//?#2>\"\"6>7?/0$;//'$8#:v# )(#'#(\"'#&'#"),I("%;//?#2T\"\"6T7U/0$;//'$8#:w# )(#'#(\"'#&'#"),I("%;//?#2V\"\"6V7W/0$;//'$8#:x# )(#'#(\"'#&'#"),I('%2h""6h7i/0#;//\'$8":y" )("\'#&\'#'),I('%;//6#2f""6f7g/\'$8":z" )("\'#&\'#'),I("%;//?#2D\"\"6D7E/0$;//'$8#:{# )(#'#(\"'#&'#"),I("%;//?#22\"\"6273/0$;//'$8#:|# )(#'#(\"'#&'#"),I("%;//?#28\"\"6879/0$;//'$8#:}# )(#'#(\"'#&'#"),I("%;//0#;&/'$8\":~\" )(\"'#&'#"),I("%;&/0#;//'$8\":~\" )(\"'#&'#"),I("%;=/T#$;G.) &;K.# &;F0/*;G.) &;K.# &;F&/,$;>/#$+#)(#'#(\"'#&'#"),I('4\x7f""5!7\x80.A &4\x81""5!7\x82.5 &4\x83""5!7\x84.) &;3.# &;.'),I("%%;//Q#;&/H$$;J.# &;K0)*;J.# &;K&/,$;&/#$+$)($'#(#'#(\"'#&'#/\"!&,)"),I("%;//]#;&/T$%$;J.# &;K0)*;J.# &;K&/\"!&,)/1$;&/($8$:\x85$!!)($'#(#'#(\"'#&'#"),I(';..G &2L""6L7M.; &4\x86""5!7\x87./ &4\x83""5!7\x84.# &;3'),I('%2j""6j7k/J#4\x88""5!7\x89.5 &4\x8a""5!7\x8b.) &4\x8c""5!7\x8d/#$+")("\'#&\'#'),I("%;N/M#28\"\"6879/>$;O.\" &\"/0$;S/'$8$:\x8e$ )($'#(#'#(\"'#&'#"),I("%;N/d#28\"\"6879/U$;O.\" &\"/G$;S/>$;_/5$;l.\" &\"/'$8&:\x8f& )(&'#(%'#($'#(#'#(\"'#&'#"),I('%3\x90""5$7\x91.) &3\x92""5#7\x93/\' 8!:\x94!! )'),I('%;P/]#%28""6879/,#;R/#$+")("\'#&\'#." &"/6$2:""6:7;/\'$8#:\x95# )(#\'#("\'#&\'#'),I("$;+.) &;-.# &;Q/2#0/*;+.) &;-.# &;Q&&&#"),I('2<""6<7=.q &2>""6>7?.e &2@""6@7A.Y &2B""6B7C.M &2D""6D7E.A &22""6273.5 &26""6677.) &24""6475'),I('%$;+._ &;-.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E0e*;+._ &;-.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E&/& 8!:\x96! )'),I('%;T/J#%28""6879/,#;^/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I("%;U.) &;\\.# &;X/& 8!:\x97! )"),I('%$%;V/2#2J""6J7K/#$+")("\'#&\'#0<*%;V/2#2J""6J7K/#$+")("\'#&\'#&/D#;W/;$2J""6J7K." &"/\'$8#:\x98# )(#\'#("\'#&\'#'),I('$4\x99""5!7\x9a/,#0)*4\x99""5!7\x9a&&&#'),I('%4$""5!7%/?#$4\x9b""5!7\x9c0)*4\x9b""5!7\x9c&/#$+")("\'#&\'#'),I('%2l""6l7m/?#;Y/6$2n""6n7o/\'$8#:\x9d# )(#\'#("\'#&\'#'),I('%%;Z/\xb3#28""6879/\xa4$;Z/\x9b$28""6879/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+-)(-\'#(,\'#(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0790 &%2\x9e""6\x9e7\x9f/\xa4#;Z/\x9b$28""6879/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+,)(,\'#(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u06f9 &%2\x9e""6\x9e7\x9f/\x8c#;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+*)(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u067a &%2\x9e""6\x9e7\x9f/t#;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0613 &%2\x9e""6\x9e7\x9f/\\#;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+&)(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u05c4 &%2\x9e""6\x9e7\x9f/D#;Z/;$28""6879/,$;[/#$+$)($\'#(#\'#("\'#&\'#.\u058d &%2\x9e""6\x9e7\x9f/,#;[/#$+")("\'#&\'#.\u056e &%2\x9e""6\x9e7\x9f/,#;Z/#$+")("\'#&\'#.\u054f &%;Z/\x9b#2\x9e""6\x9e7\x9f/\x8c$;Z/\x83$28""6879/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$++)(+\'#(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u04c7 &%;Z/\xaa#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x83$2\x9e""6\x9e7\x9f/t$;Z/k$28""6879/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+*)(*\'#()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0430 &%;Z/\xb9#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x92$%28""6879/,#;Z/#$+")("\'#&\'#." &"/k$2\x9e""6\x9e7\x9f/\\$;Z/S$28""6879/D$;Z/;$28""6879/,$;[/#$+))()\'#((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u038a &%;Z/\xc8#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xa1$%28""6879/,#;Z/#$+")("\'#&\'#." &"/z$%28""6879/,#;Z/#$+")("\'#&\'#." &"/S$2\x9e""6\x9e7\x9f/D$;Z/;$28""6879/,$;[/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u02d5 &%;Z/\xd7#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xb0$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x89$%28""6879/,#;Z/#$+")("\'#&\'#." &"/b$%28""6879/,#;Z/#$+")("\'#&\'#." &"/;$2\x9e""6\x9e7\x9f/,$;[/#$+\')(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0211 &%;Z/\xfe#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xd7$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xb0$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x89$%28""6879/,#;Z/#$+")("\'#&\'#." &"/b$%28""6879/,#;Z/#$+")("\'#&\'#." &"/;$2\x9e""6\x9e7\x9f/,$;Z/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#.\u0126 &%;Z/\u011c#%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xf5$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xce$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\xa7$%28""6879/,#;Z/#$+")("\'#&\'#." &"/\x80$%28""6879/,#;Z/#$+")("\'#&\'#." &"/Y$%28""6879/,#;Z/#$+")("\'#&\'#." &"/2$2\x9e""6\x9e7\x9f/#$+()((\'#(\'\'#(&\'#(%\'#($\'#(#\'#("\'#&\'#/& 8!:\xa0! )'),I('%;#/M#;#." &"/?$;#." &"/1$;#." &"/#$+$)($\'#(#\'#("\'#&\'#'),I("%;Z/;#28\"\"6879/,$;Z/#$+#)(#'#(\"'#&'#.# &;\\"),I("%;]/o#2J\"\"6J7K/`$;]/W$2J\"\"6J7K/H$;]/?$2J\"\"6J7K/0$;]/'$8':\xa1' )(''#(&'#(%'#($'#(#'#(\"'#&'#"),I('%2\xa2""6\xa27\xa3/2#4\xa4""5!7\xa5/#$+")("\'#&\'#.\x98 &%2\xa6""6\xa67\xa7/;#4\xa8""5!7\xa9/,$;!/#$+#)(#\'#("\'#&\'#.j &%2\xaa""6\xaa7\xab/5#;!/,$;!/#$+#)(#\'#("\'#&\'#.B &%4\xac""5!7\xad/,#;!/#$+")("\'#&\'#.# &;!'),I('%%;!." &"/[#;!." &"/M$;!." &"/?$;!." &"/1$;!." &"/#$+%)(%\'#($\'#(#\'#("\'#&\'#/\' 8!:\xae!! )'),I('$%22""6273/,#;`/#$+")("\'#&\'#0<*%22""6273/,#;`/#$+")("\'#&\'#&'),I(";a.A &;b.; &;c.5 &;d./ &;e.) &;f.# &;g"),I('%3\xaf""5*7\xb0/a#3\xb1""5#7\xb2.G &3\xb3""5#7\xb4.; &3\xb5""5$7\xb6./ &3\xb7""5#7\xb8.# &;6/($8":\xb9"! )("\'#&\'#'),I('%3\xba""5%7\xbb/I#3\xbc""5%7\xbd./ &3\xbe""5"7\xbf.# &;6/($8":\xc0"! )("\'#&\'#'),I('%3\xc1""5\'7\xc2/1#;\x90/($8":\xc3"! )("\'#&\'#'),I('%3\xc4""5$7\xc5/1#;\xf0/($8":\xc6"! )("\'#&\'#'),I('%3\xc7""5&7\xc8/1#;T/($8":\xc9"! )("\'#&\'#'),I('%3\xca""5"7\xcb/N#%2>""6>7?/,#;6/#$+")("\'#&\'#." &"/\'$8":\xcc" )("\'#&\'#'),I('%;h/P#%2>""6>7?/,#;i/#$+")("\'#&\'#." &"/)$8":\xcd""! )("\'#&\'#'),I('%$;j/�#*;j&&&#/"!&,)'),I('%$;j/�#*;j&&&#/"!&,)'),I(";k.) &;+.# &;-"),I('2l""6l7m.e &2n""6n7o.Y &24""6475.M &28""6879.A &2<""6<7=.5 &2@""6@7A.) &2B""6B7C'),I('%26""6677/n#;m/e$$%2<""6<7=/,#;m/#$+")("\'#&\'#0<*%2<""6<7=/,#;m/#$+")("\'#&\'#&/#$+#)(#\'#("\'#&\'#'),I('%;n/A#2>""6>7?/2$;o/)$8#:\xce#"" )(#\'#("\'#&\'#'),I("$;p.) &;+.# &;-/2#0/*;p.) &;+.# &;-&&&#"),I("$;p.) &;+.# &;-0/*;p.) &;+.# &;-&"),I('2l""6l7m.e &2n""6n7o.Y &24""6475.M &26""6677.A &28""6879.5 &2@""6@7A.) &2B""6B7C'),I(";\x91.# &;r"),I("%;\x90/G#;'/>$;s/5$;'/,$;\x84/#$+%)(%'#($'#(#'#(\"'#&'#"),I(";M.# &;t"),I("%;\x7f/E#28\"\"6879/6$;u.# &;x/'$8#:\xcf# )(#'#(\"'#&'#"),I('%;v.# &;w/J#%26""6677/,#;\x83/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I('%2\xd0""6\xd07\xd1/:#;\x80/1$;w." &"/#$+#)(#\'#("\'#&\'#'),I('%24""6475/,#;{/#$+")("\'#&\'#'),I("%;z/3#$;y0#*;y&/#$+\")(\"'#&'#"),I(";*.) &;+.# &;-"),I(';+.\x8f &;-.\x89 &22""6273.} &26""6677.q &28""6879.e &2:""6:7;.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I('%;|/e#$%24""6475/,#;|/#$+")("\'#&\'#0<*%24""6475/,#;|/#$+")("\'#&\'#&/#$+")("\'#&\'#'),I('%$;~0#*;~&/e#$%22""6273/,#;}/#$+")("\'#&\'#0<*%22""6273/,#;}/#$+")("\'#&\'#&/#$+")("\'#&\'#'),I("$;~0#*;~&"),I(';+.w &;-.q &28""6879.e &2:""6:7;.Y &2<""6<7=.M &2>""6>7?.A &2@""6@7A.5 &2B""6B7C.) &2D""6D7E'),I('%%;"/\x87#$;".G &;!.A &2@""6@7A.5 &2F""6F7G.) &2J""6J7K0M*;".G &;!.A &2@""6@7A.5 &2F""6F7G.) &2J""6J7K&/#$+")("\'#&\'#/& 8!:\xd2! )'),I(";\x81.# &;\x82"),I('%%;O/2#2:""6:7;/#$+")("\'#&\'#." &"/,#;S/#$+")("\'#&\'#." &"'),I('$;+.\x83 &;-.} &2B""6B7C.q &2D""6D7E.e &22""6273.Y &28""6879.M &2:""6:7;.A &2<""6<7=.5 &2>""6>7?.) &2@""6@7A/\x8c#0\x89*;+.\x83 &;-.} &2B""6B7C.q &2D""6D7E.e &22""6273.Y &28""6879.M &2:""6:7;.A &2<""6<7=.5 &2>""6>7?.) &2@""6@7A&&&#'),I("$;y0#*;y&"),I('%3\x92""5#7\xd3/q#24""6475/b$$;!/�#*;!&&&#/L$2J""6J7K/=$$;!/�#*;!&&&#/\'$8%:\xd4% )(%\'#($\'#(#\'#("\'#&\'#'),I('2\xd5""6\xd57\xd6'),I('2\xd7""6\xd77\xd8'),I('2\xd9""6\xd97\xda'),I('2\xdb""6\xdb7\xdc'),I('2\xdd""6\xdd7\xde'),I('2\xdf""6\xdf7\xe0'),I('2\xe1""6\xe17\xe2'),I('2\xe3""6\xe37\xe4'),I('2\xe5""6\xe57\xe6'),I('2\xe7""6\xe77\xe8'),I('2\xe9""6\xe97\xea'),I("%;\x85.Y &;\x86.S &;\x88.M &;\x89.G &;\x8a.A &;\x8b.; &;\x8c.5 &;\x8f./ &;\x8d.) &;\x8e.# &;6/& 8!:\xeb! )"),I("%;\x84/G#;'/>$;\x92/5$;'/,$;\x94/#$+%)(%'#($'#(#'#(\"'#&'#"),I("%;\x93/' 8!:\xec!! )"),I("%;!/5#;!/,$;!/#$+#)(#'#(\"'#&'#"),I("%$;*.A &;+.; &;-.5 &;3./ &;4.) &;'.# &;(0G*;*.A &;+.; &;-.5 &;3./ &;4.) &;'.# &;(&/& 8!:\xed! )"),I("%;\xb6/Y#$%;A/,#;\xb6/#$+\")(\"'#&'#06*%;A/,#;\xb6/#$+\")(\"'#&'#&/#$+\")(\"'#&'#"),I('%;9/N#%2:""6:7;/,#;9/#$+")("\'#&\'#." &"/\'$8":\xee" )("\'#&\'#'),I("%;:.c &%;\x98/Y#$%;A/,#;\x98/#$+\")(\"'#&'#06*%;A/,#;\x98/#$+\")(\"'#&'#&/#$+\")(\"'#&'#/& 8!:\xef! )"),I("%;L.# &;\x99/]#$%;B/,#;\x9b/#$+\")(\"'#&'#06*%;B/,#;\x9b/#$+\")(\"'#&'#&/'$8\":\xf0\" )(\"'#&'#"),I("%;\x9a.\" &\"/>#;@/5$;M/,$;?/#$+$)($'#(#'#(\"'#&'#"),I("%%;6/Y#$%;./,#;6/#$+\")(\"'#&'#06*%;./,#;6/#$+\")(\"'#&'#&/#$+\")(\"'#&'#.# &;H/' 8!:\xf1!! )"),I(";\x9c.) &;\x9d.# &;\xa0"),I("%3\xf2\"\"5!7\xf3/:#;$;\xcf/5$;./,$;\x90/#$+%)(%'#($'#(#'#(\"'#&'#"),I("%$;!/�#*;!&&&#/' 8!:\u014b!! )"),I("%;\xd1/]#$%;A/,#;\xd1/#$+\")(\"'#&'#06*%;A/,#;\xd1/#$+\")(\"'#&'#&/'$8\":\u014c\" )(\"'#&'#"),I("%;\x99/]#$%;B/,#;\xa0/#$+\")(\"'#&'#06*%;B/,#;\xa0/#$+\")(\"'#&'#&/'$8\":\u014d\" )(\"'#&'#"),I('%;L.O &;\x99.I &%;@." &"/:#;t/1$;?." &"/#$+#)(#\'#("\'#&\'#/]#$%;B/,#;\xa0/#$+")("\'#&\'#06*%;B/,#;\xa0/#$+")("\'#&\'#&/\'$8":\u014e" )("\'#&\'#'),I("%;\xd4/]#$%;B/,#;\xd5/#$+\")(\"'#&'#06*%;B/,#;\xd5/#$+\")(\"'#&'#&/'$8\":\u014f\" )(\"'#&'#"),I("%;\x96/& 8!:\u0150! )"),I('%3\u0151""5(7\u0152/:#;$;6/5$;;/,$;\xec/#$+%)(%'#($'#(#'#(\"'#&'#"),I('%3\x92""5#7\xd3.# &;6/\' 8!:\u018b!! )'),I('%3\xb1""5#7\u018c.G &3\xb3""5#7\u018d.; &3\xb7""5#7\u018e./ &3\xb5""5$7\u018f.# &;6/\' 8!:\u0190!! )'),I('%;\xee/D#%;C/,#;\xef/#$+")("\'#&\'#." &"/#$+")("\'#&\'#'),I("%;U.) &;\\.# &;X/& 8!:\u0191! )"),I('%%;!." &"/[#;!." &"/M$;!." &"/?$;!." &"/1$;!." &"/#$+%)(%\'#($\'#(#\'#("\'#&\'#/\' 8!:\u0192!! )'),I('%%;!/?#;!." &"/1$;!." &"/#$+#)(#\'#("\'#&\'#/\' 8!:\u0193!! )'),I(";\xbe"),I('%;\x9e/^#$%;B/,#;\xf3/#$+")("\'#&\'#06*%;B/,#;\xf3/#$+")("\'#&\'#&/($8":\u0194"!!)("\'#&\'#'),I(";\xf4.# &;\xa0"),I('%2\u0195""6\u01957\u0196/L#;""6>7?'),I('%;\u0100/b#28""6879/S$;\xfb/J$%2\u01a3""6\u01a37\u01a4/,#;\xec/#$+")("\'#&\'#." &"/#$+$)($\'#(#\'#("\'#&\'#'),I('%3\u01a5""5%7\u01a6.) &3\u01a7""5$7\u01a8/\' 8!:\u01a1!! )'),I('%3\xb1""5#7\xb2.6 &3\xb3""5#7\xb4.* &$;+0#*;+&/\' 8!:\u01a9!! )'),I("%;\u0104/\x87#2F\"\"6F7G/x$;\u0103/o$2F\"\"6F7G/`$;\u0103/W$2F\"\"6F7G/H$;\u0103/?$2F\"\"6F7G/0$;\u0105/'$8):\u01aa) )()'#(('#(''#(&'#(%'#($'#(#'#(\"'#&'#"),I("%;#/>#;#/5$;#/,$;#/#$+$)($'#(#'#(\"'#&'#"),I("%;\u0103/,#;\u0103/#$+\")(\"'#&'#"),I("%;\u0103/5#;\u0103/,$;\u0103/#$+#)(#'#(\"'#&'#"),I("%;q/T#$;m0#*;m&/D$%; /,#;\xf8/#$+\")(\"'#&'#.\" &\"/#$+#)(#'#(\"'#&'#"),I('%2\u01ab""6\u01ab7\u01ac.) &2\u01ad""6\u01ad7\u01ae/w#;0/n$;\u0108/e$$%;B/2#;\u0109.# &;\xa0/#$+")("\'#&\'#0<*%;B/2#;\u0109.# &;\xa0/#$+")("\'#&\'#&/#$+$)($\'#(#\'#("\'#&\'#'),I(";\x99.# &;L"),I("%2\u01af\"\"6\u01af7\u01b0/5#;g&&(g=c,u=[]),u.push(e))}function $(e,t,s){return new S(S.buildMessage(e,t),e,t,s)}function I(e){return e.split("").map((e=>e.charCodeAt(0)-32))}if(t.data={},l=function t(i){const r=a[i];let n=0;const d=[];let l=r.length;const g=[],u=[];let f;for(;;){for(;nc?(l=n+3+r[n+1],n+=3):(l=n+3+r[n+1]+r[n+2],n+=3+r[n+1]);break;case 18:g.push(l),d.push(n+4+r[n+2]+r[n+3]),e.substr(c,o[r[n+1]].length)===o[r[n+1]]?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 19:g.push(l),d.push(n+4+r[n+2]+r[n+3]),e.substr(c,o[r[n+1]].length).toLowerCase()===o[r[n+1]]?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 20:g.push(l),d.push(n+4+r[n+2]+r[n+3]),o[r[n+1]].test(e.charAt(c))?(l=n+4+r[n+2],n+=4):(l=n+4+r[n+2]+r[n+3],n+=4+r[n+2]);break;case 21:u.push(e.substr(c,r[n+1])),c+=r[n+1],n+=2;break;case 22:u.push(o[r[n+1]]),c+=o[r[n+1]].length,n+=2;break;case 23:u.push(s),0===p&&E(o[r[n+1]]),n+=2;break;case 24:h=u[u.length-1-r[n+1]],n+=2;break;case 25:h=c,n++;break;case 26:f=r.slice(n+4,n+4+r[n+3]).map((function(e){return u[u.length-1-e]})),u.splice(u.length-r[n+2],r[n+2],o[r[n+1]].apply(null,f)),n+=4+r[n+3];break;case 27:u.push(t(r[n+1])),n+=2;break;case 28:p++,n++;break;case 29:p--,n++;break;default:throw new Error("Invalid opcode: "+r[n]+".")}if(!(g.length>0))break;l=g.pop(),n=d.pop()}return u[0]}(n),l!==s&&c===e.length)return l;throw l!==s&&c=this.headers[e].length)return;const s=this.headers[e][t],i=s.raw;if(s.parsed)return s.parsed;const r=y.parse(i,e.replace(/-/g,"_"));return-1===r?void this.headers[e].splice(t,1):(s.parsed=r,r)}s(e,t=0){return this.parseHeader(e,t)}setHeader(e,t){this.headers[A(e)]=[{raw:t}]}toString(){return this.data}}class P extends H{constructor(){super()}}class x extends H{constructor(){super()}}class k{constructor(e,t,s,i,r,n,o){this.headers={},this.extraHeaders=[],this.options=k.getDefaultOptions(),r&&(this.options=Object.assign(Object.assign({},this.options),r),this.options.optionTags&&this.options.optionTags.length&&(this.options.optionTags=this.options.optionTags.slice()),this.options.routeSet&&this.options.routeSet.length&&(this.options.routeSet=this.options.routeSet.slice())),n&&n.length&&(this.extraHeaders=n.slice()),o&&(this.body={body:o.content,contentType:o.contentType}),this.method=e,this.ruri=t.clone(),this.fromURI=s.clone(),this.fromTag=this.options.fromTag?this.options.fromTag:C(),this.from=k.makeNameAddrHeader(this.fromURI,this.options.fromDisplayName,this.fromTag),this.toURI=i.clone(),this.toTag=this.options.toTag,this.to=k.makeNameAddrHeader(this.toURI,this.options.toDisplayName,this.toTag),this.callId=this.options.callId?this.options.callId:this.options.callIdPrefix+$(15),this.cseq=this.options.cseq,this.setHeader("route",this.options.routeSet),this.setHeader("via",""),this.setHeader("to",this.to.toString()),this.setHeader("from",this.from.toString()),this.setHeader("cseq",this.cseq+" "+this.method),this.setHeader("call-id",this.callId),this.setHeader("max-forwards","70")}static getDefaultOptions(){return{callId:"",callIdPrefix:"",cseq:1,toDisplayName:"",toTag:"",fromDisplayName:"",fromTag:"",forceRport:!1,hackViaTcp:!1,optionTags:["outbound"],routeSet:[],userAgentString:"sip.js",viaHost:""}}static makeNameAddrHeader(e,t,s){const i={};return s&&(i.tag=s),new v(e,t,i)}getHeader(e){const t=this.headers[A(e)];if(t){if(t[0])return t[0]}else{const t=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)if(t.test(e))return e.substring(e.indexOf(":")+1).trim()}}getHeaders(e){const t=[],s=this.headers[A(e)];if(s)for(const e of s)t.push(e);else{const s=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)s.test(e)&&t.push(e.substring(e.indexOf(":")+1).trim())}return t}hasHeader(e){if(this.headers[A(e)])return!0;{const t=new RegExp("^\\s*"+e+"\\s*:","i");for(const e of this.extraHeaders)if(t.test(e))return!0}return!1}setHeader(e,t){this.headers[A(e)]=t instanceof Array?t:[t]}setViaHeader(e,t){this.options.hackViaTcp&&(t="TCP");let s="SIP/2.0/"+t;s+=" "+this.options.viaHost+";branch="+e,this.options.forceRport&&(s+=";rport"),this.setHeader("via",s),this.branch=e}toString(){let e="";e+=this.method+" "+this.ruri.toRaw()+" SIP/2.0\r\n";for(const t in this.headers)if(this.headers[t])for(const s of this.headers[t])e+=t+": "+s+"\r\n";for(const t of this.extraHeaders)e+=t.trim()+"\r\n";return e+="Supported: "+this.options.optionTags.join(", ")+"\r\n",e+="User-Agent: "+this.options.userAgentString+"\r\n",this.body?"string"==typeof this.body?(e+="Content-Length: "+D(this.body)+"\r\n\r\n",e+=this.body):this.body.body&&this.body.contentType?(e+="Content-Type: "+this.body.contentType+"\r\n",e+="Content-Length: "+D(this.body.body)+"\r\n\r\n",e+=this.body.body):e+="Content-Length: 0\r\n\r\n":e+="Content-Length: 0\r\n\r\n",e}}function _(e){return"application/sdp"===e?"session":"render"}function q(e){const t="string"==typeof e?e:e.body,s="string"==typeof e?"application/sdp":e.contentType;return{contentDisposition:_(s),contentType:s,content:t}}function M(e){return!(!e||"string"!=typeof e.content||"string"!=typeof e.contentType||void 0!==e.contentDisposition)||"string"==typeof e.contentDisposition}function N(e){let t,s,i;if(e instanceof P&&e.body){const r=e.parseHeader("Content-Disposition");t=r?r.type:void 0,s=e.parseHeader("Content-Type"),i=e.body}if(e instanceof x&&e.body){const r=e.parseHeader("Content-Disposition");t=r?r.type:void 0,s=e.parseHeader("Content-Type"),i=e.body}if(e instanceof k&&e.body)if(t=e.getHeader("Content-Disposition"),s=e.getHeader("Content-Type"),"string"==typeof e.body){if(!s)throw new Error("Header content type header does not equal body content type.");i=e.body}else{if(s&&s!==e.body.contentType)throw new Error("Header content type header does not equal body content type.");s=e.body.contentType,i=e.body.body}if(M(e)&&(t=e.contentDisposition,s=e.contentType,i=e.content),i){if(s&&!t&&(t=_(s)),!t)throw new Error("Content disposition undefined.");if(!s)throw new Error("Content type undefined.");return{contentDisposition:t,contentType:s,content:i}}}var O,U;!function(e){e.Initial="Initial",e.Early="Early",e.AckWait="AckWait",e.Confirmed="Confirmed",e.Terminated="Terminated"}(O=O||(O={})),function(e){e.Initial="Initial",e.HaveLocalOffer="HaveLocalOffer",e.HaveRemoteOffer="HaveRemoteOffer",e.Stable="Stable",e.Closed="Closed"}(U=U||(U={}));const j=500,F=5e3,L={T1:j,T2:4e3,T4:F,TIMER_B:32e3,TIMER_D:0,TIMER_F:32e3,TIMER_H:32e3,TIMER_I:0,TIMER_J:0,TIMER_K:0,TIMER_L:32e3,TIMER_M:32e3,TIMER_N:32e3,PROVISIONAL_RESPONSE_INTERVAL:6e4};class B extends n{constructor(e){super(e||"Transaction state error.")}}var G;!function(e){e.ACK="ACK",e.BYE="BYE",e.CANCEL="CANCEL",e.INFO="INFO",e.INVITE="INVITE",e.MESSAGE="MESSAGE",e.NOTIFY="NOTIFY",e.OPTIONS="OPTIONS",e.REGISTER="REGISTER",e.UPDATE="UPDATE",e.SUBSCRIBE="SUBSCRIBE",e.PUBLISH="PUBLISH",e.REFER="REFER",e.PRACK="PRACK"}(G=G||(G={}));const V=[G.ACK,G.BYE,G.CANCEL,G.INFO,G.INVITE,G.MESSAGE,G.NOTIFY,G.OPTIONS,G.PRACK,G.REFER,G.REGISTER,G.SUBSCRIBE];class W{constructor(e){this.incomingMessageRequest=e}get request(){return this.incomingMessageRequest.message}accept(e){return this.incomingMessageRequest.accept(e),Promise.resolve()}reject(e){return this.incomingMessageRequest.reject(e),Promise.resolve()}}class K{constructor(e){this.incomingNotifyRequest=e}get request(){return this.incomingNotifyRequest.message}accept(e){return this.incomingNotifyRequest.accept(e),Promise.resolve()}reject(e){return this.incomingNotifyRequest.reject(e),Promise.resolve()}}class Y{constructor(e,t){this.incomingReferRequest=e,this.session=t}get referTo(){const e=this.incomingReferRequest.message.parseHeader("refer-to");if(!(e instanceof v))throw new Error("Failed to parse Refer-To header.");return e}get referredBy(){return this.incomingReferRequest.message.getHeader("referred-by")}get replaces(){const e=this.referTo.uri.getHeader("replaces");return e instanceof Array?e[0]:e}get request(){return this.incomingReferRequest.message}accept(e={statusCode:202}){return this.incomingReferRequest.accept(e),Promise.resolve()}reject(e){return this.incomingReferRequest.reject(e),Promise.resolve()}makeInviter(e){if(this.inviter)return this.inviter;const t=this.referTo.uri.clone();t.clearHeaders();const s=((e=e||{}).extraHeaders||[]).slice(),i=this.replaces;i&&s.push("Replaces: "+decodeURIComponent(i));const r=this.referredBy;return r&&s.push("Referred-By: "+r),e.extraHeaders=s,this.inviter=this.session.userAgent._makeInviter(t,e),this.inviter._referred=this.session,this.session._referral=this.inviter,this.inviter}}var Z,J;!function(e){e.Initial="Initial",e.Establishing="Establishing",e.Established="Established",e.Terminating="Terminating",e.Terminated="Terminated"}(Z=Z||(Z={}));class z{constructor(e,t={}){this.pendingReinvite=!1,this.pendingReinviteAck=!1,this._state=Z.Initial,this.delegate=t.delegate,this._stateEventEmitter=new p,this._userAgent=e}dispose(){switch(this.logger.log(`Session ${this.id} in state ${this._state} is being disposed`),delete this.userAgent._sessions[this.id],this._sessionDescriptionHandler&&this._sessionDescriptionHandler.close(),this.state){case Z.Initial:case Z.Establishing:break;case Z.Established:return new Promise((e=>{this._bye({onAccept:()=>e(),onRedirect:()=>e(),onReject:()=>e()})}));case Z.Terminating:case Z.Terminated:break;default:throw new Error("Unknown state.")}return Promise.resolve()}get assertedIdentity(){return this._assertedIdentity}get dialog(){return this._dialog}get id(){return this._id}get replacee(){return this._replacee}get sessionDescriptionHandler(){return this._sessionDescriptionHandler}get sessionDescriptionHandlerFactory(){return this.userAgent.configuration.sessionDescriptionHandlerFactory}get sessionDescriptionHandlerModifiers(){return this._sessionDescriptionHandlerModifiers||[]}set sessionDescriptionHandlerModifiers(e){this._sessionDescriptionHandlerModifiers=e.slice()}get sessionDescriptionHandlerOptions(){return this._sessionDescriptionHandlerOptions||{}}set sessionDescriptionHandlerOptions(e){this._sessionDescriptionHandlerOptions=Object.assign({},e)}get sessionDescriptionHandlerModifiersReInvite(){return this._sessionDescriptionHandlerModifiersReInvite||[]}set sessionDescriptionHandlerModifiersReInvite(e){this._sessionDescriptionHandlerModifiersReInvite=e.slice()}get sessionDescriptionHandlerOptionsReInvite(){return this._sessionDescriptionHandlerOptionsReInvite||{}}set sessionDescriptionHandlerOptionsReInvite(e){this._sessionDescriptionHandlerOptionsReInvite=Object.assign({},e)}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get userAgent(){return this._userAgent}bye(e={}){let t="Session.bye() may only be called if established session.";switch(this.state){case Z.Initial:"function"==typeof this.cancel?(t+=" However Inviter.invite() has not yet been called.",t+=" Perhaps you should have called Inviter.cancel()?"):"function"==typeof this.reject&&(t+=" However Invitation.accept() has not yet been called.",t+=" Perhaps you should have called Invitation.reject()?");break;case Z.Establishing:"function"==typeof this.cancel?(t+=" However a dialog does not yet exist.",t+=" Perhaps you should have called Inviter.cancel()?"):"function"==typeof this.reject&&(t+=" However Invitation.accept() has not yet been called (or not yet resolved).",t+=" Perhaps you should have called Invitation.reject()?");break;case Z.Established:{const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._bye(t,s)}case Z.Terminating:t+=" However this session is already terminating.","function"==typeof this.cancel?t+=" Perhaps you have already called Inviter.cancel()?":"function"==typeof this.reject&&(t+=" Perhaps you have already called Session.bye()?");break;case Z.Terminated:t+=" However this session is already terminated.";break;default:throw new Error("Unknown state")}return this.logger.error(t),Promise.reject(new Error(`Invalid session state ${this.state}`))}info(e={}){if(this.state!==Z.Established){const e="Session.info() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._info(t,s)}invite(e={}){if(this.logger.log("Session.invite"),this.state!==Z.Established)return Promise.reject(new Error(`Invalid session state ${this.state}`));if(this.pendingReinvite)return Promise.reject(new a("Reinvite in progress. Please wait until complete, then try again."));this.pendingReinvite=!0,e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiersReInvite=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptionsReInvite=e.sessionDescriptionHandlerOptions);const t={onAccept:t=>{const s=N(t.message);if(!s)return this.logger.error("Received 2xx response to re-INVITE without a session description"),this.ackAndBye(t,400,"Missing session description"),this.stateTransition(Z.Terminated),void(this.pendingReinvite=!1);if(e.withoutSdp){const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.setOfferAndGetAnswer(s,i).then((e=>{t.ack({body:e})})).catch((e=>{this.logger.error("Failed to handle offer in 2xx response to re-INVITE"),this.logger.error(e.message),this.state===Z.Terminated?t.ack():(this.ackAndBye(t,488,"Bad Media Description"),this.stateTransition(Z.Terminated))})).then((()=>{this.pendingReinvite=!1,e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))}else{const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.setAnswer(s,i).then((()=>{t.ack()})).catch((e=>{this.logger.error("Failed to handle answer in 2xx response to re-INVITE"),this.logger.error(e.message),this.state!==Z.Terminated?(this.ackAndBye(t,488,"Bad Media Description"),this.stateTransition(Z.Terminated)):t.ack()})).then((()=>{this.pendingReinvite=!1,e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))}},onProgress:e=>{},onRedirect:e=>{},onReject:t=>{this.logger.warn("Received a non-2xx response to re-INVITE"),this.pendingReinvite=!1,e.withoutSdp?e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t):this.rollbackOffer().catch((e=>{if(this.logger.error("Failed to rollback offer on non-2xx response to re-INVITE"),this.logger.error(e.message),this.state!==Z.Terminated){if(!this.dialog)throw new Error("Dialog undefined.");const e=[];e.push("Reason: "+this.getReasonHeaderValue(500,"Internal Server Error")),this.dialog.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated)}})).then((()=>{e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t)}))},onTrying:e=>{}},s=e.requestOptions||{};if(s.extraHeaders=(s.extraHeaders||[]).slice(),s.extraHeaders.push("Allow: "+V.toString()),s.extraHeaders.push("Contact: "+this._contact),e.withoutSdp){if(!this.dialog)throw this.pendingReinvite=!1,new Error("Dialog undefined.");return Promise.resolve(this.dialog.invite(t,s))}const i={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};return this.getOffer(i).then((e=>{if(!this.dialog)throw this.pendingReinvite=!1,new Error("Dialog undefined.");return s.body=e,this.dialog.invite(t,s)})).catch((e=>{throw this.logger.error(e.message),this.logger.error("Failed to send re-INVITE"),this.pendingReinvite=!1,e}))}message(e={}){if(this.state!==Z.Established){const e="Session.message() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const t=e.requestDelegate,s=this.copyRequestOptions(e.requestOptions);return this._message(t,s)}refer(e,t={}){if(this.state!==Z.Established){const e="Session.refer() may only be called if established session.";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}if(e instanceof z&&!e.dialog){const e="Session.refer() may only be called with session which is established. You are perhaps attempting to attended transfer to a target for which there is not dialog yet established. Perhaps you are attempting a 'semi-attended' tansfer? Regardless, this is not supported. The recommended approached is to check to see if the target Session is in the Established state before calling refer(); if the state is not Established you may proceed by falling back using a URI as the target (blind transfer).";return this.logger.error(e),Promise.reject(new Error(`Invalid session state ${this.state}`))}const s=t.requestDelegate,i=this.copyRequestOptions(t.requestOptions);return i.extraHeaders=i.extraHeaders?i.extraHeaders.concat(this.referExtraHeaders(this.referToString(e))):this.referExtraHeaders(this.referToString(e)),this._refer(t.onNotify,s,i)}_bye(e,t){if(!this.dialog)return Promise.reject(new Error("Session dialog undefined."));const s=this.dialog;switch(s.sessionState){case O.Initial:case O.Early:throw new Error(`Invalid dialog state ${s.sessionState}`);case O.AckWait:return this.stateTransition(Z.Terminating),new Promise((i=>{s.delegate={onAck:()=>{const r=s.bye(e,t);return this.stateTransition(Z.Terminated),i(r),Promise.resolve()},onAckTimeout:()=>{const r=s.bye(e,t);this.stateTransition(Z.Terminated),i(r)}}}));case O.Confirmed:{const i=s.bye(e,t);return this.stateTransition(Z.Terminated),Promise.resolve(i)}case O.Terminated:throw new Error(`Invalid dialog state ${s.sessionState}`);default:throw new Error("Unrecognized state.")}}_info(e,t){return this.dialog?Promise.resolve(this.dialog.info(e,t)):Promise.reject(new Error("Session dialog undefined."))}_message(e,t){return this.dialog?Promise.resolve(this.dialog.message(e,t)):Promise.reject(new Error("Session dialog undefined."))}_refer(e,t,s){return this.dialog?(this.onNotify=e,Promise.resolve(this.dialog.refer(t,s))):Promise.reject(new Error("Session dialog undefined."))}ackAndBye(e,t,s){e.ack();const i=[];t&&i.push("Reason: "+this.getReasonHeaderValue(t,s)),e.session.bye(void 0,{extraHeaders:i})}onAckRequest(e){if(this.logger.log("Session.onAckRequest"),this.state!==Z.Established&&this.state!==Z.Terminating)return this.logger.error(`ACK received while in state ${this.state}, dropping request`),Promise.resolve();const t=this.dialog;if(!t)throw new Error("Dialog undefined.");const s={sessionDescriptionHandlerOptions:this.pendingReinviteAck?this.sessionDescriptionHandlerOptionsReInvite:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.pendingReinviteAck?this._sessionDescriptionHandlerModifiersReInvite:this._sessionDescriptionHandlerModifiers};if(this.delegate&&this.delegate.onAck){const t=new l(e);this.delegate.onAck(t)}switch(this.pendingReinviteAck=!1,t.signalingState){case U.Initial:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.Stable:{const i=N(e.message);return i?"render"===i.contentDisposition?(this._renderbody=i.content,this._rendertype=i.contentType,Promise.resolve()):"session"!==i.contentDisposition?Promise.resolve():this.setAnswer(i,s).catch((e=>{this.logger.error(e.message);const s=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];t.bye(void 0,{extraHeaders:s}),this.stateTransition(Z.Terminated)})):Promise.resolve()}case U.HaveLocalOffer:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.HaveRemoteOffer:{this.logger.error(`Invalid signaling state ${t.signalingState}.`);const e=["Reason: "+this.getReasonHeaderValue(488,"Bad Media Description")];return t.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated),Promise.resolve()}case U.Closed:default:throw new Error(`Invalid signaling state ${t.signalingState}.`)}}onByeRequest(e){if(this.logger.log("Session.onByeRequest"),this.state===Z.Established){if(this.delegate&&this.delegate.onBye){const t=new g(e);this.delegate.onBye(t)}else e.accept();this.stateTransition(Z.Terminated)}else this.logger.error(`BYE received while in state ${this.state}, dropping request`)}onInfoRequest(e){if(this.logger.log("Session.onInfoRequest"),this.state===Z.Established)if(this.delegate&&this.delegate.onInfo){const t=new f(e);this.delegate.onInfo(t)}else e.accept();else this.logger.error(`INFO received while in state ${this.state}, dropping request`)}onInviteRequest(e){if(this.logger.log("Session.onInviteRequest"),this.state!==Z.Established)return void this.logger.error(`INVITE received while in state ${this.state}, dropping request`);this.pendingReinviteAck=!0;const t=["Contact: "+this._contact];if(e.message.hasHeader("P-Asserted-Identity")){const t=e.message.getHeader("P-Asserted-Identity");if(!t)throw new Error("Header undefined.");this._assertedIdentity=y.nameAddrHeaderParse(t)}const s={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptionsReInvite,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiersReInvite};this.generateResponseOfferAnswerInDialog(s).then((s=>{const i=e.accept({statusCode:200,extraHeaders:t,body:s});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,i.message,200)})).catch((t=>{if(this.logger.error(t.message),this.logger.error("Failed to handle to re-INVITE request"),!this.dialog)throw new Error("Dialog undefined.");if(this.logger.error(this.dialog.signalingState),this.dialog.signalingState!==U.Stable)this.rollbackOffer().then((()=>{const t=e.reject({statusCode:488});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,t.message,488)})).catch((t=>{this.logger.error(t.message),this.logger.error("Failed to rollback offer on re-INVITE request");const s=e.reject({statusCode:488});if(this.state!==Z.Terminated){if(!this.dialog)throw new Error("Dialog undefined.");const e=[];e.push("Reason: "+this.getReasonHeaderValue(500,"Internal Server Error")),this.dialog.bye(void 0,{extraHeaders:e}),this.stateTransition(Z.Terminated)}this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,s.message,488)}));else{const t=e.reject({statusCode:488});this.delegate&&this.delegate.onInvite&&this.delegate.onInvite(e.message,t.message,488)}}))}onMessageRequest(e){if(this.logger.log("Session.onMessageRequest"),this.state===Z.Established)if(this.delegate&&this.delegate.onMessage){const t=new W(e);this.delegate.onMessage(t)}else e.accept();else this.logger.error(`MESSAGE received while in state ${this.state}, dropping request`)}onNotifyRequest(e){if(this.logger.log("Session.onNotifyRequest"),this.state===Z.Established)if(this.onNotify){const t=new K(e);this.onNotify(t)}else if(this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else e.accept();else this.logger.error(`NOTIFY received while in state ${this.state}, dropping request`)}onPrackRequest(e){if(this.logger.log("Session.onPrackRequest"),this.state===Z.Established)throw new Error("Unimplemented.");this.logger.error(`PRACK received while in state ${this.state}, dropping request`)}onReferRequest(e){if(this.logger.log("Session.onReferRequest"),this.state!==Z.Established)return void this.logger.error(`REFER received while in state ${this.state}, dropping request`);if(!e.message.hasHeader("refer-to"))return this.logger.warn("Invalid REFER packet. A refer-to header is required. Rejecting."),void e.reject();const t=new Y(e,this);this.delegate&&this.delegate.onRefer?this.delegate.onRefer(t):(this.logger.log("No delegate available to handle REFER, automatically accepting and following."),t.accept().then((()=>t.makeInviter(this._referralInviterOptions).invite())).catch((e=>{this.logger.error(e.message)})))}generateResponseOfferAnswer(e,t){if(this.dialog)return this.generateResponseOfferAnswerInDialog(t);const s=N(e.message);return s&&"session"===s.contentDisposition?this.setOfferAndGetAnswer(s,t):this.getOffer(t)}generateResponseOfferAnswerInDialog(e){if(!this.dialog)throw new Error("Dialog undefined.");switch(this.dialog.signalingState){case U.Initial:return this.getOffer(e);case U.HaveLocalOffer:return Promise.resolve(void 0);case U.HaveRemoteOffer:if(!this.dialog.offer)throw new Error(`Session offer undefined in signaling state ${this.dialog.signalingState}.`);return this.setOfferAndGetAnswer(this.dialog.offer,e);case U.Stable:return this.state!==Z.Established?Promise.resolve(void 0):this.getOffer(e);case U.Closed:default:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`)}}getOffer(e){const t=this.setupSessionDescriptionHandler(),s=e.sessionDescriptionHandlerOptions,i=e.sessionDescriptionHandlerModifiers;try{return t.getDescription(s,i).then((e=>q(e))).catch((e=>{this.logger.error("Session.getOffer: SDH getDescription rejected...");const t=e instanceof Error?e:new Error("Session.getOffer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.getOffer: SDH getDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}rollbackOffer(){const e=this.setupSessionDescriptionHandler();if(void 0===e.rollbackDescription)return Promise.resolve();try{return e.rollbackDescription().catch((e=>{this.logger.error("Session.rollbackOffer: SDH rollbackDescription rejected...");const t=e instanceof Error?e:new Error("Session.rollbackOffer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.rollbackOffer: SDH rollbackDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setAnswer(e,t){const s=this.setupSessionDescriptionHandler(),i=t.sessionDescriptionHandlerOptions,r=t.sessionDescriptionHandlerModifiers;try{if(!s.hasDescription(e.contentType))return Promise.reject(new o)}catch(e){this.logger.error("Session.setAnswer: SDH hasDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}try{return s.setDescription(e.content,i,r).catch((e=>{this.logger.error("Session.setAnswer: SDH setDescription rejected...");const t=e instanceof Error?e:new Error("Session.setAnswer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.setAnswer: SDH setDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setOfferAndGetAnswer(e,t){const s=this.setupSessionDescriptionHandler(),i=t.sessionDescriptionHandlerOptions,r=t.sessionDescriptionHandlerModifiers;try{if(!s.hasDescription(e.contentType))return Promise.reject(new o)}catch(e){this.logger.error("Session.setOfferAndGetAnswer: SDH hasDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}try{return s.setDescription(e.content,i,r).then((()=>s.getDescription(i,r))).then((e=>q(e))).catch((e=>{this.logger.error("Session.setOfferAndGetAnswer: SDH setDescription or getDescription rejected...");const t=e instanceof Error?e:new Error("Session.setOfferAndGetAnswer unknown error.");throw this.logger.error(t.message),t}))}catch(e){this.logger.error("Session.setOfferAndGetAnswer: SDH setDescription or getDescription threw...");const t=e instanceof Error?e:new Error(e);return this.logger.error(t.message),Promise.reject(t)}}setSessionDescriptionHandler(e){if(this._sessionDescriptionHandler)throw new Error("Session description handler defined.");this._sessionDescriptionHandler=e}setupSessionDescriptionHandler(){var e;return this._sessionDescriptionHandler||(this._sessionDescriptionHandler=this.sessionDescriptionHandlerFactory(this,this.userAgent.configuration.sessionDescriptionHandlerFactoryOptions),(null===(e=this.delegate)||void 0===e?void 0:e.onSessionDescriptionHandler)&&this.delegate.onSessionDescriptionHandler(this._sessionDescriptionHandler,!1)),this._sessionDescriptionHandler}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case Z.Initial:e!==Z.Establishing&&e!==Z.Established&&e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Establishing:e!==Z.Established&&e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Established:e!==Z.Terminating&&e!==Z.Terminated&&t();break;case Z.Terminating:e!==Z.Terminated&&t();break;case Z.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Session ${this.id} transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===Z.Terminated&&this.dispose()}copyRequestOptions(e={}){return{extraHeaders:e.extraHeaders?e.extraHeaders.slice():void 0,body:e.body?{contentDisposition:e.body.contentDisposition||"render",contentType:e.body.contentType||"text/plain",content:e.body.content||""}:void 0}}getReasonHeaderValue(e,t){const s=e;let i=I(e);return!i&&t&&(i=t),"SIP;cause="+s+';text="'+i+'"'}referExtraHeaders(e){const t=[];return t.push("Referred-By: <"+this.userAgent.configuration.uri+">"),t.push("Contact: "+this._contact),t.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),t.push("Refer-To: "+e),t}referToString(e){let t;if(e instanceof w)t=e.toString();else{if(!e.dialog)throw new Error("Dialog undefined.");const s=e.remoteIdentity.friendlyName,i=e.dialog.remoteTarget.toString(),r=e.dialog.callId,n=e.dialog.remoteTag,o=e.dialog.localTag;t=`"${s}" <${i}?Replaces=${encodeURIComponent(`${r};to-tag=${n};from-tag=${o}`)}>`}return t}}!function(e){e.Required="Required",e.Supported="Supported",e.Unsupported="Unsupported"}(J=J||(J={}));const X={"100rel":!0,199:!0,answermode:!0,"early-session":!0,eventlist:!0,explicitsub:!0,"from-change":!0,"geolocation-http":!0,"geolocation-sip":!0,gin:!0,gruu:!0,histinfo:!0,ice:!0,join:!0,"multiple-refer":!0,norefersub:!0,nosub:!0,outbound:!0,path:!0,policy:!0,precondition:!0,pref:!0,privacy:!0,"recipient-list-invite":!0,"recipient-list-message":!0,"recipient-list-subscribe":!0,replaces:!0,"resource-priority":!0,"sdp-anat":!0,"sec-agree":!0,tdialog:!0,timer:!0,uui:!0};class Q extends z{constructor(e,t){super(e),this.incomingInviteRequest=t,this.disposed=!1,this.expiresTimer=void 0,this.isCanceled=!1,this.rel100="none",this.rseq=Math.floor(1e4*Math.random()),this.userNoAnswerTimer=void 0,this.waitingForPrack=!1,this.logger=e.getLogger("sip.Invitation");const s=this.incomingInviteRequest.message,i=s.getHeader("require");i&&i.toLowerCase().includes("100rel")&&(this.rel100="required");const r=s.getHeader("supported");if(r&&r.toLowerCase().includes("100rel")&&(this.rel100="supported"),s.toTag=t.toTag,"string"!=typeof s.toTag)throw new TypeError("toTag should have been a string.");if(this.userNoAnswerTimer=setTimeout((()=>{t.reject({statusCode:480}),this.stateTransition(Z.Terminated)}),this.userAgent.configuration.noAnswerTimeout?1e3*this.userAgent.configuration.noAnswerTimeout:6e4),s.hasHeader("expires")){const e=1e3*Number(s.getHeader("expires")||0);this.expiresTimer=setTimeout((()=>{this.state===Z.Initial&&(t.reject({statusCode:487}),this.stateTransition(Z.Terminated))}),e)}const n=this.request.getHeader("P-Asserted-Identity");n&&(this._assertedIdentity=y.nameAddrHeaderParse(n)),this._contact=this.userAgent.contact.toString();const o=s.parseHeader("Content-Disposition");o&&"render"===o.type&&(this._renderbody=s.body,this._rendertype=s.getHeader("Content-Type")),this._id=s.callId+s.fromTag,this.userAgent._sessions[this._id]=this}dispose(){if(this.disposed)return Promise.resolve();switch(this.disposed=!0,this.expiresTimer&&(clearTimeout(this.expiresTimer),this.expiresTimer=void 0),this.userNoAnswerTimer&&(clearTimeout(this.userNoAnswerTimer),this.userNoAnswerTimer=void 0),this.prackNeverArrived(),this.state){case Z.Initial:case Z.Establishing:return this.reject().then((()=>super.dispose()));case Z.Established:case Z.Terminating:case Z.Terminated:return super.dispose();default:throw new Error("Unknown state.")}}get autoSendAnInitialProvisionalResponse(){return"required"!==this.rel100&&this.userAgent.configuration.sendInitialProvisionalResponse}get body(){return this.incomingInviteRequest.message.body}get localIdentity(){return this.request.to}get remoteIdentity(){return this.request.from}get request(){return this.incomingInviteRequest.message}accept(e={}){if(this.logger.log("Invitation.accept"),this.state!==Z.Initial){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}return e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),this.stateTransition(Z.Establishing),this.sendAccept(e).then((({message:e,session:t})=>{t.delegate={onAck:e=>this.onAckRequest(e),onAckTimeout:()=>this.onAckTimeout(),onBye:e=>this.onByeRequest(e),onInfo:e=>this.onInfoRequest(e),onInvite:e=>this.onInviteRequest(e),onMessage:e=>this.onMessageRequest(e),onNotify:e=>this.onNotifyRequest(e),onPrack:e=>this.onPrackRequest(e),onRefer:e=>this.onReferRequest(e)},this._dialog=t,this.stateTransition(Z.Established),this._replacee&&this._replacee._bye()})).catch((e=>this.handleResponseError(e)))}progress(e={}){if(this.logger.log("Invitation.progress"),this.state!==Z.Initial){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}const t=e.statusCode||180;if(t<100||t>199)throw new TypeError("Invalid statusCode: "+t);return e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),this.waitingForPrack?(this.logger.warn("Unexpected call for progress while waiting for prack, ignoring"),Promise.resolve()):100===e.statusCode?this.sendProgressTrying().then((()=>{})).catch((e=>this.handleResponseError(e))):"required"===this.rel100||"supported"===this.rel100&&e.rel100||"supported"===this.rel100&&this.userAgent.configuration.sipExtension100rel===J.Required?this.sendProgressReliableWaitForPrack(e).then((()=>{})).catch((e=>this.handleResponseError(e))):this.sendProgress(e).then((()=>{})).catch((e=>this.handleResponseError(e)))}reject(e={}){if(this.logger.log("Invitation.reject"),this.state!==Z.Initial&&this.state!==Z.Establishing){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}const t=e.statusCode||480,s=e.reasonPhrase?e.reasonPhrase:I(t),i=e.extraHeaders||[];if(t<300||t>699)throw new TypeError("Invalid statusCode: "+t);const r=e.body?q(e.body):void 0;return t<400?this.incomingInviteRequest.redirect([],{statusCode:t,reasonPhrase:s,extraHeaders:i,body:r}):this.incomingInviteRequest.reject({statusCode:t,reasonPhrase:s,extraHeaders:i,body:r}),this.stateTransition(Z.Terminated),Promise.resolve()}_onCancel(e){if(this.logger.log("Invitation._onCancel"),this.state===Z.Initial||this.state===Z.Establishing){if(this.delegate&&this.delegate.onCancel){const t=new u(e);this.delegate.onCancel(t)}this.isCanceled=!0,this.incomingInviteRequest.reject({statusCode:487}),this.stateTransition(Z.Terminated)}else this.logger.error(`CANCEL received while in state ${this.state}, dropping request`)}handlePrackOfferAnswer(e){if(!this.dialog)throw new Error("Dialog undefined.");const t=N(e.message);if(!t||"session"!==t.contentDisposition)return Promise.resolve(void 0);const s={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers};switch(this.dialog.signalingState){case U.Initial:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);case U.Stable:return this.setAnswer(t,s).then((()=>{}));case U.HaveLocalOffer:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`);case U.HaveRemoteOffer:return this.setOfferAndGetAnswer(t,s);case U.Closed:default:throw new Error(`Invalid signaling state ${this.dialog.signalingState}.`)}}handleResponseError(e){let t=480;if(e instanceof Error?this.logger.error(e.message):this.logger.error(e),e instanceof o?(this.logger.error("A session description handler occurred while sending response (content type unsupported"),t=415):e instanceof c?this.logger.error("A session description handler occurred while sending response"):e instanceof h?this.logger.error("Session ended before response could be formulated and sent (while waiting for PRACK)"):e instanceof B&&this.logger.error("Session changed state before response could be formulated and sent"),this.state===Z.Initial||this.state===Z.Establishing)try{this.incomingInviteRequest.reject({statusCode:t}),this.stateTransition(Z.Terminated)}catch(e){throw this.logger.error("An error occurred attempting to reject the request while handling another error"),e}if(!this.isCanceled)throw e;this.logger.warn("An error occurred while attempting to formulate and send a response to an incoming INVITE. However a CANCEL was received and processed while doing so which can (and often does) result in errors occurring as the session terminates in the meantime. Said error is being ignored.")}onAckTimeout(){if(this.logger.log("Invitation.onAckTimeout"),!this.dialog)throw new Error("Dialog undefined.");this.logger.log("No ACK received for an extended period of time, terminating session"),this.dialog.bye(),this.stateTransition(Z.Terminated)}sendAccept(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.extraHeaders||[];return this.waitingForPrack?this.waitForArrivalOfPrack().then((()=>clearTimeout(this.userNoAnswerTimer))).then((()=>this.generateResponseOfferAnswer(this.incomingInviteRequest,t))).then((e=>this.incomingInviteRequest.accept({statusCode:200,body:e,extraHeaders:s}))):(clearTimeout(this.userNoAnswerTimer),this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>this.incomingInviteRequest.accept({statusCode:200,body:e,extraHeaders:s}))))}sendProgress(e={}){const t=e.statusCode||180,s=e.reasonPhrase,i=(e.extraHeaders||[]).slice(),r=e.body?q(e.body):void 0;if(183===t&&!r)return this.sendProgressWithSDP(e);try{const e=this.incomingInviteRequest.progress({statusCode:t,reasonPhrase:s,extraHeaders:i,body:r});return this._dialog=e.session,Promise.resolve(e)}catch(e){return Promise.reject(e)}}sendProgressWithSDP(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.statusCode||183,i=e.reasonPhrase,r=(e.extraHeaders||[]).slice();return this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:e}))).then((e=>(this._dialog=e.session,e)))}sendProgressReliable(e={}){return e.extraHeaders=(e.extraHeaders||[]).slice(),e.extraHeaders.push("Require: 100rel"),e.extraHeaders.push("RSeq: "+Math.floor(1e4*Math.random())),this.sendProgressWithSDP(e)}sendProgressReliableWaitForPrack(e={}){const t={sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions,sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers},s=e.statusCode||183,i=e.reasonPhrase,r=(e.extraHeaders||[]).slice();let n;return r.push("Require: 100rel"),r.push("RSeq: "+this.rseq++),new Promise(((e,o)=>{this.waitingForPrack=!0,this.generateResponseOfferAnswer(this.incomingInviteRequest,t).then((e=>(n=e,this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:n})))).then((t=>{let a,c;this._dialog=t.session,t.session.delegate={onPrack:s=>{a=s,clearTimeout(d),clearTimeout(u),this.waitingForPrack&&(this.waitingForPrack=!1,this.handlePrackOfferAnswer(a).then((s=>{try{c=a.accept({statusCode:200,body:s}),this.prackArrived(),e({prackRequest:a,prackResponse:c,progressResponse:t})}catch(e){o(e)}})).catch((e=>o(e))))}};const d=setTimeout((()=>{this.waitingForPrack&&(this.waitingForPrack=!1,this.logger.warn("No PRACK received, rejecting INVITE."),clearTimeout(u),this.reject({statusCode:504}).then((()=>o(new h))).catch((e=>o(e))))}),64*L.T1),l=()=>{try{this.incomingInviteRequest.progress({statusCode:s,reasonPhrase:i,extraHeaders:r,body:n})}catch(e){return this.waitingForPrack=!1,void o(e)}u=setTimeout(l,g*=2)};let g=L.T1,u=setTimeout(l,g)})).catch((e=>{this.waitingForPrack=!1,o(e)}))}))}sendProgressTrying(){try{const e=this.incomingInviteRequest.trying();return Promise.resolve(e)}catch(e){return Promise.reject(e)}}waitForArrivalOfPrack(){if(this.waitingForPrackPromise)throw new Error("Already waiting for PRACK");return this.waitingForPrackPromise=new Promise(((e,t)=>{this.waitingForPrackResolve=e,this.waitingForPrackReject=t})),this.waitingForPrackPromise}prackArrived(){this.waitingForPrackResolve&&this.waitingForPrackResolve(),this.waitingForPrackPromise=void 0,this.waitingForPrackResolve=void 0,this.waitingForPrackReject=void 0}prackNeverArrived(){this.waitingForPrackReject&&this.waitingForPrackReject(new h),this.waitingForPrackPromise=void 0,this.waitingForPrackResolve=void 0,this.waitingForPrackReject=void 0}}class ee extends z{constructor(e,t,s={}){super(e,s),this.disposed=!1,this.earlyMedia=!1,this.earlyMediaSessionDescriptionHandlers=new Map,this.isCanceled=!1,this.inviteWithoutSdp=!1,this.logger=e.getLogger("sip.Inviter"),this.earlyMedia=void 0!==s.earlyMedia?s.earlyMedia:this.earlyMedia,this.fromTag=C(),this.inviteWithoutSdp=void 0!==s.inviteWithoutSdp?s.inviteWithoutSdp:this.inviteWithoutSdp;const i=Object.assign({},s);i.params=Object.assign({},s.params);const r=s.anonymous||!1,n=e.contact.toString({anonymous:r,outbound:r?!e.contact.tempGruu:!e.contact.pubGruu});r&&e.configuration.uri&&(i.params.fromDisplayName="Anonymous",i.params.fromUri="sip:anonymous@anonymous.invalid");let o=e.userAgentCore.configuration.aor;if(i.params.fromUri&&(o="string"==typeof i.params.fromUri?y.URIParse(i.params.fromUri):i.params.fromUri),!o)throw new TypeError("Invalid from URI: "+i.params.fromUri);let a=t;if(i.params.toUri&&(a="string"==typeof i.params.toUri?y.URIParse(i.params.toUri):i.params.toUri),!a)throw new TypeError("Invalid to URI: "+i.params.toUri);const c=Object.assign({},i.params);c.fromTag=this.fromTag;const h=(i.extraHeaders||[]).slice();r&&e.configuration.uri&&(h.push("P-Preferred-Identity: "+e.configuration.uri.toString()),h.push("Privacy: id")),h.push("Contact: "+n),h.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),e.configuration.sipExtension100rel===J.Required&&h.push("Require: 100rel"),e.configuration.sipExtensionReplaces===J.Required&&h.push("Require: replaces"),i.extraHeaders=h;this.outgoingRequestMessage=e.userAgentCore.makeOutgoingRequestMessage(G.INVITE,t,o,a,c,h,undefined),this._contact=n,this._referralInviterOptions=i,this._renderbody=s.renderbody,this._rendertype=s.rendertype,s.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=s.sessionDescriptionHandlerModifiers),s.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=s.sessionDescriptionHandlerOptions),s.sessionDescriptionHandlerModifiersReInvite&&(this.sessionDescriptionHandlerModifiersReInvite=s.sessionDescriptionHandlerModifiersReInvite),s.sessionDescriptionHandlerOptionsReInvite&&(this.sessionDescriptionHandlerOptionsReInvite=s.sessionDescriptionHandlerOptionsReInvite),this._id=this.outgoingRequestMessage.callId+this.fromTag,this.userAgent._sessions[this._id]=this}dispose(){if(this.disposed)return Promise.resolve();switch(this.disposed=!0,this.disposeEarlyMedia(),this.state){case Z.Initial:case Z.Establishing:return this.cancel().then((()=>super.dispose()));case Z.Established:case Z.Terminating:case Z.Terminated:return super.dispose();default:throw new Error("Unknown state.")}}get body(){return this.outgoingRequestMessage.body}get localIdentity(){return this.outgoingRequestMessage.from}get remoteIdentity(){return this.outgoingRequestMessage.to}get request(){return this.outgoingRequestMessage}cancel(e={}){if(this.logger.log("Inviter.cancel"),this.state!==Z.Initial&&this.state!==Z.Establishing){const e=new Error(`Invalid session state ${this.state}`);return this.logger.error(e.message),Promise.reject(e)}if(this.isCanceled=!0,this.stateTransition(Z.Terminating),this.outgoingInviteRequest){let t;e.statusCode&&e.reasonPhrase&&(t=function(e,t){if(e&&e<200||e>699)throw new TypeError("Invalid statusCode: "+e);if(e)return"SIP;cause="+e+';text="'+(I(e)||t)+'"'}(e.statusCode,e.reasonPhrase)),this.outgoingInviteRequest.cancel(t,e)}else this.logger.warn("Canceled session before INVITE was sent"),this.stateTransition(Z.Terminated);return Promise.resolve()}invite(e={}){if(this.logger.log("Inviter.invite"),this.state!==Z.Initial)return super.invite(e);if(e.sessionDescriptionHandlerModifiers&&(this.sessionDescriptionHandlerModifiers=e.sessionDescriptionHandlerModifiers),e.sessionDescriptionHandlerOptions&&(this.sessionDescriptionHandlerOptions=e.sessionDescriptionHandlerOptions),e.withoutSdp||this.inviteWithoutSdp)return this._renderbody&&this._rendertype&&(this.outgoingRequestMessage.body={contentType:this._rendertype,body:this._renderbody}),this.stateTransition(Z.Establishing),Promise.resolve(this.sendInvite(e));const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.getOffer(t).then((t=>(this.outgoingRequestMessage.body={body:t.content,contentType:t.contentType},this.stateTransition(Z.Establishing),this.sendInvite(e)))).catch((e=>{throw this.logger.log(e.message),this.state!==Z.Terminated&&this.stateTransition(Z.Terminated),e}))}sendInvite(e={}){return this.outgoingInviteRequest=this.userAgent.userAgentCore.invite(this.outgoingRequestMessage,{onAccept:t=>this.dialog?(this.logger.log("Additional confirmed dialog, sending ACK and BYE"),void this.ackAndBye(t)):this.isCanceled?(this.logger.log("Canceled session accepted, sending ACK and BYE"),this.ackAndBye(t),void this.stateTransition(Z.Terminated)):(this.notifyReferer(t),void this.onAccept(t).then((()=>{this.disposeEarlyMedia()})).catch((()=>{this.disposeEarlyMedia()})).then((()=>{e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t)}))),onProgress:t=>{this.isCanceled||(this.notifyReferer(t),this.onProgress(t).catch((()=>{this.disposeEarlyMedia()})).then((()=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)})))},onRedirect:t=>{this.notifyReferer(t),this.onRedirect(t),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t)},onReject:t=>{this.notifyReferer(t),this.onReject(t),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t)},onTrying:t=>{this.notifyReferer(t),this.onTrying(t),e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}}),this.outgoingInviteRequest}disposeEarlyMedia(){this.earlyMediaSessionDescriptionHandlers.forEach((e=>{e.close()})),this.earlyMediaSessionDescriptionHandlers.clear()}notifyReferer(e){if(!this._referred)return;if(!(this._referred instanceof z))throw new Error("Referred session not instance of session");if(!this._referred.dialog)return;if(!e.message.statusCode)throw new Error("Status code undefined.");if(!e.message.reasonPhrase)throw new Error("Reason phrase undefined.");const t=`SIP/2.0 ${e.message.statusCode} ${e.message.reasonPhrase}`.trim();this._referred.dialog.notify(void 0,{extraHeaders:["Event: refer","Subscription-State: terminated"],body:{contentDisposition:"render",contentType:"message/sipfrag",content:t}}).delegate={onReject:()=>{this._referred=void 0}}}onAccept(e){if(this.logger.log("Inviter.onAccept"),this.state!==Z.Establishing)return this.logger.error(`Accept received while in state ${this.state}, dropping response`),Promise.reject(new Error(`Invalid session state ${this.state}`));const t=e.message,s=e.session;switch(t.hasHeader("P-Asserted-Identity")&&(this._assertedIdentity=y.nameAddrHeaderParse(t.getHeader("P-Asserted-Identity"))),s.delegate={onAck:e=>this.onAckRequest(e),onBye:e=>this.onByeRequest(e),onInfo:e=>this.onInfoRequest(e),onInvite:e=>this.onInviteRequest(e),onMessage:e=>this.onMessageRequest(e),onNotify:e=>this.onNotifyRequest(e),onPrack:e=>this.onPrackRequest(e),onRefer:e=>this.onReferRequest(e)},this._dialog=s,s.signalingState){case U.Initial:case U.HaveLocalOffer:return this.logger.error("Received 2xx response to INVITE without a session description"),this.ackAndBye(e,400,"Missing session description"),this.stateTransition(Z.Terminated),Promise.reject(new Error("Bad Media Description"));case U.HaveRemoteOffer:{if(!this._dialog.offer)throw new Error(`Session offer undefined in signaling state ${this._dialog.signalingState}.`);const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setOfferAndGetAnswer(this._dialog.offer,t).then((t=>{e.ack({body:t}),this.stateTransition(Z.Established)})).catch((t=>{throw this.ackAndBye(e,488,"Invalid session description"),this.stateTransition(Z.Terminated),t}))}case U.Stable:{if(this.earlyMediaSessionDescriptionHandlers.size>0){const t=this.earlyMediaSessionDescriptionHandlers.get(s.id);if(!t)throw new Error("Session description handler undefined.");return this.setSessionDescriptionHandler(t),this.earlyMediaSessionDescriptionHandlers.delete(s.id),e.ack(),this.stateTransition(Z.Established),Promise.resolve()}if(this.earlyMediaDialog){if(this.earlyMediaDialog!==s){if(this.earlyMedia){const e="You have set the 'earlyMedia' option to 'true' which requires that your INVITE requests do not fork and yet this INVITE request did in fact fork. Consequentially and not surprisingly the end point which accepted the INVITE (confirmed dialog) does not match the end point with which early media has been setup (early dialog) and thus this session is unable to proceed. In accordance with the SIP specifications, the SIP servers your end point is connected to determine if an INVITE forks and the forking behavior of those servers cannot be controlled by this library. If you wish to use early media with this library you must configure those servers accordingly. Alternatively you may set the 'earlyMedia' to 'false' which will allow this library to function with any INVITE requests which do fork.";this.logger.error(e)}const t=new Error("Early media dialog does not equal confirmed dialog, terminating session");return this.logger.error(t.message),this.ackAndBye(e,488,"Not Acceptable Here"),this.stateTransition(Z.Terminated),Promise.reject(t)}return e.ack(),this.stateTransition(Z.Established),Promise.resolve()}const t=s.answer;if(!t)throw new Error("Answer is undefined.");const i={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setAnswer(t,i).then((()=>{let t;this._renderbody&&this._rendertype&&(t={body:{contentDisposition:"render",contentType:this._rendertype,content:this._renderbody}}),e.ack(t),this.stateTransition(Z.Established)})).catch((t=>{throw this.logger.error(t.message),this.ackAndBye(e,488,"Not Acceptable Here"),this.stateTransition(Z.Terminated),t}))}case U.Closed:return Promise.reject(new Error("Terminated."));default:throw new Error("Unknown session signaling state.")}}onProgress(e){var t;if(this.logger.log("Inviter.onProgress"),this.state!==Z.Establishing)return this.logger.error(`Progress received while in state ${this.state}, dropping response`),Promise.reject(new Error(`Invalid session state ${this.state}`));if(!this.outgoingInviteRequest)throw new Error("Outgoing INVITE request undefined.");const s=e.message,i=e.session;s.hasHeader("P-Asserted-Identity")&&(this._assertedIdentity=y.nameAddrHeaderParse(s.getHeader("P-Asserted-Identity")));const r=s.getHeader("require"),n=s.getHeader("rseq"),o=!!(r&&r.includes("100rel")&&n?Number(n):void 0),a=[];switch(o&&a.push("RAck: "+s.getHeader("rseq")+" "+s.getHeader("cseq")),i.signalingState){case U.Initial:return o&&(this.logger.warn("First reliable provisional response received MUST contain an offer when INVITE does not contain an offer."),e.prack({extraHeaders:a})),Promise.resolve();case U.HaveLocalOffer:return o&&e.prack({extraHeaders:a}),Promise.resolve();case U.HaveRemoteOffer:if(!o)return this.logger.warn("Non-reliable provisional response MUST NOT contain an initial offer, discarding response."),Promise.resolve();{const r=this.sessionDescriptionHandlerFactory(this,this.userAgent.configuration.sessionDescriptionHandlerFactoryOptions||{});return(null===(t=this.delegate)||void 0===t?void 0:t.onSessionDescriptionHandler)&&this.delegate.onSessionDescriptionHandler(r,!0),this.earlyMediaSessionDescriptionHandlers.set(i.id,r),r.setDescription(s.body,this.sessionDescriptionHandlerOptions,this.sessionDescriptionHandlerModifiers).then((()=>r.getDescription(this.sessionDescriptionHandlerOptions,this.sessionDescriptionHandlerModifiers))).then((t=>{const s={contentDisposition:"session",contentType:t.contentType,content:t.body};e.prack({extraHeaders:a,body:s})})).catch((e=>{throw this.stateTransition(Z.Terminated),e}))}case U.Stable:if(o&&e.prack({extraHeaders:a}),this.earlyMedia&&!this.earlyMediaDialog){this.earlyMediaDialog=i;const e=i.answer;if(!e)throw new Error("Answer is undefined.");const t={sessionDescriptionHandlerModifiers:this.sessionDescriptionHandlerModifiers,sessionDescriptionHandlerOptions:this.sessionDescriptionHandlerOptions};return this.setAnswer(e,t).catch((e=>{throw this.stateTransition(Z.Terminated),e}))}return Promise.resolve();case U.Closed:return Promise.reject(new Error("Terminated."));default:throw new Error("Unknown session signaling state.")}}onRedirect(e){this.logger.log("Inviter.onRedirect"),this.state===Z.Establishing||this.state===Z.Terminating?this.stateTransition(Z.Terminated):this.logger.error(`Redirect received while in state ${this.state}, dropping response`)}onReject(e){this.logger.log("Inviter.onReject"),this.state===Z.Establishing||this.state===Z.Terminating?this.stateTransition(Z.Terminated):this.logger.error(`Reject received while in state ${this.state}, dropping response`)}onTrying(e){this.logger.log("Inviter.onTrying"),this.state===Z.Establishing||this.logger.error(`Trying received while in state ${this.state}, dropping response`)}}class te{constructor(e,t,s,i="text/plain",r={}){this.logger=e.getLogger("sip.Messager"),r.params=r.params||{};let n=e.userAgentCore.configuration.aor;if(r.params.fromUri&&(n="string"==typeof r.params.fromUri?y.URIParse(r.params.fromUri):r.params.fromUri),!n)throw new TypeError("Invalid from URI: "+r.params.fromUri);let o=t;if(r.params.toUri&&(o="string"==typeof r.params.toUri?y.URIParse(r.params.toUri):r.params.toUri),!o)throw new TypeError("Invalid to URI: "+r.params.toUri);const a=r.params?Object.assign({},r.params):{},c=(r.extraHeaders||[]).slice(),h={contentDisposition:"render",contentType:i,content:s};this.request=e.userAgentCore.makeOutgoingRequestMessage(G.MESSAGE,t,n,o,a,c,h),this.userAgent=e}message(e={}){return this.userAgent.userAgentCore.request(this.request,e.requestDelegate),Promise.resolve()}}var se,ie,re,ne,oe,ae,ce,he,de;!function(e){e.Initial="Initial",e.Published="Published",e.Unpublished="Unpublished",e.Terminated="Terminated"}(se=se||(se={}));class le{constructor(e,t,s,i={}){this.disposed=!1,this._state=se.Initial,this._stateEventEmitter=new p,this.userAgent=e,i.extraHeaders=(i.extraHeaders||[]).slice(),i.contentType=i.contentType||"text/plain","number"!=typeof i.expires||i.expires%1!=0?i.expires=3600:i.expires=Number(i.expires),"boolean"!=typeof i.unpublishOnClose&&(i.unpublishOnClose=!0),this.target=t,this.event=s,this.options=i,this.pubRequestExpires=i.expires,this.logger=e.getLogger("sip.Publisher");const r=i.params||{},n=r.fromUri?r.fromUri:e.userAgentCore.configuration.aor,o=r.toUri?r.toUri:t;let a;if(i.body&&i.contentType){a={contentDisposition:"render",contentType:i.contentType,content:i.body}}const c=(i.extraHeaders||[]).slice();this.request=e.userAgentCore.makeOutgoingRequestMessage(G.PUBLISH,t,n,o,r,c,a),this.id=this.target.toString()+":"+this.event,this.userAgent._publishers[this.id]=this}dispose(){return this.disposed?Promise.resolve():(this.disposed=!0,this.logger.log(`Publisher ${this.id} in state ${this.state} is being disposed`),delete this.userAgent._publishers[this.id],this.options.unpublishOnClose&&this.state===se.Published?this.unpublish():(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestExpires=0,this.pubRequestEtag=void 0,Promise.resolve()))}get state(){return this._state}get stateChange(){return this._stateEventEmitter}publish(e,t={}){if(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.options.body=e,this.pubRequestBody=this.options.body,0===this.pubRequestExpires){if(void 0===this.options.expires)throw new Error("Expires undefined.");this.pubRequestExpires=this.options.expires,this.pubRequestEtag=void 0}return this.sendPublishRequest(),Promise.resolve()}unpublish(e={}){return this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestExpires=0,void 0!==this.pubRequestEtag&&this.sendPublishRequest(),Promise.resolve()}receiveResponse(e){const t=e.statusCode||0;switch(!0){case/^1[0-9]{2}$/.test(t.toString()):break;case/^2[0-9]{2}$/.test(t.toString()):if(e.hasHeader("SIP-ETag")?this.pubRequestEtag=e.getHeader("SIP-ETag"):this.logger.warn("SIP-ETag header missing in a 200-class response to PUBLISH"),e.hasHeader("Expires")){const t=Number(e.getHeader("Expires"));"number"==typeof t&&t>=0&&t<=this.pubRequestExpires?this.pubRequestExpires=t:this.logger.warn("Bad Expires header in a 200-class response to PUBLISH")}else this.logger.warn("Expires header missing in a 200-class response to PUBLISH");0!==this.pubRequestExpires?(this.publishRefreshTimer=setTimeout((()=>this.refreshRequest()),900*this.pubRequestExpires),this._state!==se.Published&&this.stateTransition(se.Published)):this.stateTransition(se.Unpublished);break;case/^412$/.test(t.toString()):if(void 0!==this.pubRequestEtag&&0!==this.pubRequestExpires){if(this.logger.warn("412 response to PUBLISH, recovering"),this.pubRequestEtag=void 0,void 0===this.options.body)throw new Error("Body undefined.");this.publish(this.options.body)}else this.logger.warn("412 response to PUBLISH, recovery failed"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated);break;case/^423$/.test(t.toString()):if(0!==this.pubRequestExpires&&e.hasHeader("Min-Expires")){const t=Number(e.getHeader("Min-Expires"));if("number"==typeof t||t>this.pubRequestExpires){if(this.logger.warn("423 code in response to PUBLISH, adjusting the Expires value and trying to recover"),this.pubRequestExpires=t,void 0===this.options.body)throw new Error("Body undefined.");this.publish(this.options.body)}else this.logger.warn("Bad 423 response Min-Expires header received for PUBLISH"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated)}else this.logger.warn("423 response to PUBLISH, recovery failed"),this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated);break;default:this.pubRequestExpires=0,this.stateTransition(se.Unpublished),this.stateTransition(se.Terminated)}0===this.pubRequestExpires&&(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,this.pubRequestEtag=void 0)}send(){return this.userAgent.userAgentCore.publish(this.request,{onAccept:e=>this.receiveResponse(e.message),onProgress:e=>this.receiveResponse(e.message),onRedirect:e=>this.receiveResponse(e.message),onReject:e=>this.receiveResponse(e.message),onTrying:e=>this.receiveResponse(e.message)})}refreshRequest(){if(this.publishRefreshTimer&&(clearTimeout(this.publishRefreshTimer),this.publishRefreshTimer=void 0),this.pubRequestBody=void 0,void 0===this.pubRequestEtag)throw new Error("Etag undefined");if(0===this.pubRequestExpires)throw new Error("Expires zero");this.sendPublishRequest()}sendPublishRequest(){const e=Object.assign({},this.options);e.extraHeaders=(this.options.extraHeaders||[]).slice(),e.extraHeaders.push("Event: "+this.event),e.extraHeaders.push("Expires: "+this.pubRequestExpires),void 0!==this.pubRequestEtag&&e.extraHeaders.push("SIP-If-Match: "+this.pubRequestEtag);const t=this.target,s=this.options.params||{};let i,r;if(void 0!==this.pubRequestBody){if(void 0===this.options.contentType)throw new Error("Content type undefined.");i={body:this.pubRequestBody,contentType:this.options.contentType}}return i&&(r=q(i)),this.request=this.userAgent.userAgentCore.makeOutgoingRequestMessage(G.PUBLISH,t,s.fromUri?s.fromUri:this.userAgent.userAgentCore.configuration.aor,s.toUri?s.toUri:this.target,s,e.extraHeaders,r),this.send()}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case se.Initial:e!==se.Published&&e!==se.Unpublished&&e!==se.Terminated&&t();break;case se.Published:e!==se.Unpublished&&e!==se.Terminated&&t();break;case se.Unpublished:e!==se.Published&&e!==se.Terminated&&t();break;case se.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Publication transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===se.Terminated&&this.dispose()}}!function(e){e.Initial="Initial",e.Registered="Registered",e.Unregistered="Unregistered",e.Terminated="Terminated"}(ie=ie||(ie={}));class ge{constructor(e,t={}){this.disposed=!1,this._contacts=[],this._retryAfter=void 0,this._state=ie.Initial,this._waiting=!1,this._stateEventEmitter=new p,this._waitingEventEmitter=new p,this.userAgent=e;const s=e.configuration.uri.clone();if(s.user=void 0,this.options=Object.assign(Object.assign(Object.assign({},ge.defaultOptions()),{registrar:s}),ge.stripUndefinedProperties(t)),this.options.extraContactHeaderParams=(this.options.extraContactHeaderParams||[]).slice(),this.options.extraHeaders=(this.options.extraHeaders||[]).slice(),!this.options.registrar)throw new Error("Registrar undefined.");if(this.options.registrar=this.options.registrar.clone(),this.options.regId&&!this.options.instanceId?this.options.instanceId=this.userAgent.instanceId:!this.options.regId&&this.options.instanceId&&(this.options.regId=1),this.options.instanceId&&-1===y.parse(this.options.instanceId,"uuid"))throw new Error("Invalid instanceId.");if(this.options.regId&&this.options.regId<0)throw new Error("Invalid regId.");const i=this.options.registrar,r=this.options.params&&this.options.params.fromUri||e.userAgentCore.configuration.aor,n=this.options.params&&this.options.params.toUri||e.configuration.uri,o=this.options.params||{},a=(t.extraHeaders||[]).slice();if(this.request=e.userAgentCore.makeOutgoingRequestMessage(G.REGISTER,i,r,n,o,a,void 0),this.expires=this.options.expires||ge.defaultExpires,this.expires<0)throw new Error("Invalid expires.");if(this.refreshFrequency=this.options.refreshFrequency||ge.defaultRefreshFrequency,this.refreshFrequency<50||this.refreshFrequency>99)throw new Error("Invalid refresh frequency. The value represents a percentage of the expiration time and should be between 50 and 99.");this.logger=e.getLogger("sip.Registerer"),this.options.logConfiguration&&(this.logger.log("Configuration:"),Object.keys(this.options).forEach((e=>{const t=this.options[e];if("registrar"===e)this.logger.log("\xb7 "+e+": "+t);else this.logger.log("\xb7 "+e+": "+JSON.stringify(t))}))),this.id=this.request.callId+this.request.from.parameters.tag,this.userAgent._registerers[this.id]=this}static defaultOptions(){return{expires:ge.defaultExpires,extraContactHeaderParams:[],extraHeaders:[],logConfiguration:!0,instanceId:"",params:{},regId:0,registrar:new w("sip","anonymous","anonymous.invalid"),refreshFrequency:ge.defaultRefreshFrequency}}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}get contacts(){return this._contacts.slice()}get retryAfter(){return this._retryAfter}get state(){return this._state}get stateChange(){return this._stateEventEmitter}dispose(){return this.disposed?Promise.resolve():(this.disposed=!0,this.logger.log(`Registerer ${this.id} in state ${this.state} is being disposed`),delete this.userAgent._registerers[this.id],new Promise((e=>{const t=()=>{if(!this.waiting&&this._state===ie.Registered)return this.stateChange.addListener((()=>{this.terminated(),e()}),{once:!0}),void this.unregister();this.terminated(),e()};this.waiting?this.waitingChange.addListener((()=>{t()}),{once:!0}):t()})))}register(e={}){if(this.state===ie.Terminated)throw this.stateError(),new Error("Registerer terminated. Unable to register.");if(this.disposed)throw this.stateError(),new Error("Registerer disposed. Unable to register.");if(this.waiting){this.waitingWarning();const e=new a("REGISTER request already in progress, waiting for final response");return Promise.reject(e)}e.requestOptions&&(this.options=Object.assign(Object.assign({},this.options),e.requestOptions));const t=(this.options.extraHeaders||[]).slice();t.push("Contact: "+this.generateContactHeader(this.expires)),t.push("Allow: "+["ACK","CANCEL","INVITE","MESSAGE","BYE","OPTIONS","INFO","NOTIFY","REFER"].toString()),this.request.cseq++,this.request.setHeader("cseq",this.request.cseq+" REGISTER"),this.request.extraHeaders=t,this.waitingToggle(!0);const s=this.userAgent.userAgentCore.register(this.request,{onAccept:t=>{let s;t.message.hasHeader("expires")&&(s=Number(t.message.getHeader("expires"))),this._contacts=t.message.getHeaders("contact");let i,r=this._contacts.length;if(!r)return this.logger.error("No Contact header in response to REGISTER, dropping response."),void this.unregistered();for(;r--;){if(i=t.message.parseHeader("contact",r),!i)throw new Error("Contact undefined");if(this.userAgent.contact.pubGruu&&b(i.uri,this.userAgent.contact.pubGruu)){s=Number(i.getParam("expires"));break}if(""===this.userAgent.configuration.contactName){if(i.uri.user===this.userAgent.contact.uri.user){s=Number(i.getParam("expires"));break}}else if(b(i.uri,this.userAgent.contact.uri)){s=Number(i.getParam("expires"));break}i=void 0}if(void 0===i)return this.logger.error("No Contact header pointing to us, dropping response"),this.unregistered(),void this.waitingToggle(!1);if(void 0===s)return this.logger.error("Contact pointing to us is missing expires parameter, dropping response"),this.unregistered(),void this.waitingToggle(!1);if(i.hasParam("temp-gruu")){const e=i.getParam("temp-gruu");e&&(this.userAgent.contact.tempGruu=y.URIParse(e.replace(/"/g,"")))}if(i.hasParam("pub-gruu")){const e=i.getParam("pub-gruu");e&&(this.userAgent.contact.pubGruu=y.URIParse(e.replace(/"/g,"")))}this.registered(s),e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t),this.waitingToggle(!1)},onProgress:t=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)},onRedirect:t=>{this.logger.error("Redirect received. Not supported."),this.unregistered(),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t),this.waitingToggle(!1)},onReject:t=>{if(423===t.message.statusCode)return t.message.hasHeader("min-expires")?(this.expires=Number(t.message.getHeader("min-expires")),this.waitingToggle(!1),void this.register()):(this.logger.error("423 response received for REGISTER without Min-Expires, dropping response"),this.unregistered(),void this.waitingToggle(!1));this.logger.warn(`Failed to register, status code ${t.message.statusCode}`);let s=NaN;if(500===t.message.statusCode||503===t.message.statusCode){const e=t.message.getHeader("retry-after");e&&(s=Number.parseInt(e,void 0))}this._retryAfter=isNaN(s)?void 0:s,this.unregistered(),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t),this._retryAfter=void 0,this.waitingToggle(!1)},onTrying:t=>{e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}});return Promise.resolve(s)}unregister(e={}){if(this.state===ie.Terminated)throw this.stateError(),new Error("Registerer terminated. Unable to register.");if(this.disposed&&this.state!==ie.Registered)throw this.stateError(),new Error("Registerer disposed. Unable to register.");if(this.waiting){this.waitingWarning();const e=new a("REGISTER request already in progress, waiting for final response");return Promise.reject(e)}this._state===ie.Registered||e.all||this.logger.warn("Not currently registered, but sending an unregister anyway.");const t=(e.requestOptions&&e.requestOptions.extraHeaders||[]).slice();this.request.extraHeaders=t,e.all?(t.push("Contact: *"),t.push("Expires: 0")):t.push("Contact: "+this.generateContactHeader(0)),this.request.cseq++,this.request.setHeader("cseq",this.request.cseq+" REGISTER"),void 0!==this.registrationTimer&&(clearTimeout(this.registrationTimer),this.registrationTimer=void 0),this.waitingToggle(!0);const s=this.userAgent.userAgentCore.register(this.request,{onAccept:t=>{this._contacts=t.message.getHeaders("contact"),this.unregistered(),e.requestDelegate&&e.requestDelegate.onAccept&&e.requestDelegate.onAccept(t),this.waitingToggle(!1)},onProgress:t=>{e.requestDelegate&&e.requestDelegate.onProgress&&e.requestDelegate.onProgress(t)},onRedirect:t=>{this.logger.error("Unregister redirected. Not currently supported."),this.unregistered(),e.requestDelegate&&e.requestDelegate.onRedirect&&e.requestDelegate.onRedirect(t),this.waitingToggle(!1)},onReject:t=>{this.logger.error(`Unregister rejected with status code ${t.message.statusCode}`),this.unregistered(),e.requestDelegate&&e.requestDelegate.onReject&&e.requestDelegate.onReject(t),this.waitingToggle(!1)},onTrying:t=>{e.requestDelegate&&e.requestDelegate.onTrying&&e.requestDelegate.onTrying(t)}});return Promise.resolve(s)}clearTimers(){void 0!==this.registrationTimer&&(clearTimeout(this.registrationTimer),this.registrationTimer=void 0),void 0!==this.registrationExpiredTimer&&(clearTimeout(this.registrationExpiredTimer),this.registrationExpiredTimer=void 0)}generateContactHeader(e){let t=this.userAgent.contact.toString({register:!0});return this.options.regId&&this.options.instanceId&&(t+=";reg-id="+this.options.regId,t+=';+sip.instance=""'),this.options.extraContactHeaderParams&&this.options.extraContactHeaderParams.forEach((e=>{t+=";"+e})),t+=";expires="+e,t}registered(e){this.clearTimers(),this.registrationTimer=setTimeout((()=>{this.registrationTimer=void 0,this.register()}),this.refreshFrequency/100*e*1e3),this.registrationExpiredTimer=setTimeout((()=>{this.logger.warn("Registration expired"),this.unregistered()}),1e3*e),this._state!==ie.Registered&&this.stateTransition(ie.Registered)}unregistered(){this.clearTimers(),this._state!==ie.Unregistered&&this.stateTransition(ie.Unregistered)}terminated(){this.clearTimers(),this._state!==ie.Terminated&&this.stateTransition(ie.Terminated)}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ie.Initial:e!==ie.Registered&&e!==ie.Unregistered&&e!==ie.Terminated&&t();break;case ie.Registered:e!==ie.Unregistered&&e!==ie.Terminated&&t();break;case ie.Unregistered:e!==ie.Registered&&e!==ie.Terminated&&t();break;case ie.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state=e,this.logger.log(`Registration transitioned to state ${this._state}`),this._stateEventEmitter.emit(this._state),e===ie.Terminated&&this.dispose()}get waiting(){return this._waiting}get waitingChange(){return this._waitingEventEmitter}waitingToggle(e){if(this._waiting===e)throw new Error(`Invalid waiting transition from ${this._waiting} to ${e}`);this._waiting=e,this.logger.log(`Waiting toggled to ${this._waiting}`),this._waitingEventEmitter.emit(this._waiting)}waitingWarning(){let e="An attempt was made to send a REGISTER request while a prior one was still in progress.";e+=" RFC 3261 requires UAs MUST NOT send a new registration until they have received a final response",e+=" from the registrar for the previous one or the previous REGISTER request has timed out.",e+=" Note that if the transport disconnects, you still must wait for the prior request to time out before",e+=" sending a new REGISTER request or alternatively dispose of the current Registerer and create a new Registerer.",this.logger.warn("An attempt was made to send a REGISTER request while a prior one was still in progress. RFC 3261 requires UAs MUST NOT send a new registration until they have received a final response from the registrar for the previous one or the previous REGISTER request has timed out. Note that if the transport disconnects, you still must wait for the prior request to time out before sending a new REGISTER request or alternatively dispose of the current Registerer and create a new Registerer.")}stateError(){let e=`An attempt was made to send a REGISTER request when the Registerer ${this.state===ie.Terminated?"is in 'Terminated' state":"has been disposed"}.`;e+=" The Registerer transitions to 'Terminated' when Registerer.dispose() is called.",e+=" Perhaps you called UserAgent.stop() which dipsoses of all Registerers?",this.logger.error(e)}}ge.defaultExpires=600,ge.defaultRefreshFrequency=99,function(e){e.Initial="Initial",e.NotifyWait="NotifyWait",e.Pending="Pending",e.Active="Active",e.Terminated="Terminated"}(re=re||(re={})),function(e){e.Initial="Initial",e.NotifyWait="NotifyWait",e.Subscribed="Subscribed",e.Terminated="Terminated"}(ne=ne||(ne={}));class ue{constructor(e,t={}){this._disposed=!1,this._state=ne.Initial,this._logger=e.getLogger("sip.Subscription"),this._stateEventEmitter=new p,this._userAgent=e,this.delegate=t.delegate}dispose(){return this._disposed||(this._disposed=!0,this._stateEventEmitter.removeAllListeners()),Promise.resolve()}get dialog(){return this._dialog}get disposed(){return this._disposed}get state(){return this._state}get stateChange(){return this._stateEventEmitter}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ne.Initial:e!==ne.NotifyWait&&e!==ne.Terminated&&t();break;case ne.NotifyWait:e!==ne.Subscribed&&e!==ne.Terminated&&t();break;case ne.Subscribed:e!==ne.Terminated&&t();break;case ne.Terminated:t();break;default:throw new Error("Unrecognized state.")}this._state!==e&&(this._state=e,this._logger.log(`Subscription ${this._dialog?this._dialog.id:void 0} transitioned to ${this._state}`),this._stateEventEmitter.emit(this._state),e===ne.Terminated&&this.dispose())}}class pe extends ue{constructor(e,t,s,i={}){super(e,i),this.body=void 0,this.logger=e.getLogger("sip.Subscriber"),i.body&&(this.body={body:i.body,contentType:i.contentType?i.contentType:"application/sdp"}),this.targetURI=t,this.event=s,void 0===i.expires?this.expires=3600:"number"!=typeof i.expires?(this.logger.warn('Option "expires" must be a number. Using default of 3600.'),this.expires=3600):this.expires=i.expires,this.extraHeaders=(i.extraHeaders||[]).slice(),this.subscriberRequest=this.initSubscriberRequest(),this.outgoingRequestMessage=this.subscriberRequest.message,this.id=this.outgoingRequestMessage.callId+this.outgoingRequestMessage.from.parameters.tag+this.event,this._userAgent._subscriptions[this.id]=this}dispose(){return this.disposed?Promise.resolve():(this.logger.log(`Subscription ${this.id} in state ${this.state} is being disposed`),delete this._userAgent._subscriptions[this.id],this.retryAfterTimer&&(clearTimeout(this.retryAfterTimer),this.retryAfterTimer=void 0),this.subscriberRequest.dispose(),super.dispose().then((()=>{if(this.state===ne.Subscribed){if(!this._dialog)throw new Error("Dialog undefined.");if(this._dialog.subscriptionState===re.Pending||this._dialog.subscriptionState===re.Active){const e=this._dialog;return new Promise(((t,s)=>{e.delegate={onTerminated:()=>t()},e.unsubscribe()}))}}})))}subscribe(e={}){switch(this.subscriberRequest.state){case re.Initial:this.state===ne.Initial&&this.stateTransition(ne.NotifyWait),this.subscriberRequest.subscribe().then((e=>{e.success?(e.success.subscription&&(this._dialog=e.success.subscription,this._dialog.delegate={onNotify:e=>this.onNotify(e),onRefresh:e=>this.onRefresh(e),onTerminated:()=>{this.state!==ne.Terminated&&this.stateTransition(ne.Terminated)}}),this.onNotify(e.success.request)):e.failure&&this.unsubscribe()}));break;case re.NotifyWait:case re.Pending:break;case re.Active:if(this._dialog){this._dialog.refresh().delegate={onAccept:e=>this.onAccepted(e),onRedirect:e=>this.unsubscribe(),onReject:e=>this.unsubscribe()}}case re.Terminated:}return Promise.resolve()}unsubscribe(e={}){if(this.disposed)return Promise.resolve();switch(this.subscriberRequest.state){case re.Initial:case re.NotifyWait:break;case re.Pending:case re.Active:this._dialog&&this._dialog.unsubscribe();break;case re.Terminated:break;default:throw new Error("Unknown state.")}return this.stateTransition(ne.Terminated),Promise.resolve()}_refresh(){return this.subscriberRequest.state===re.Active?this.subscribe():Promise.resolve()}onAccepted(e){}onNotify(e){if(this.disposed)return void e.accept();if(this.state!==ne.Subscribed&&this.stateTransition(ne.Subscribed),this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else e.accept();const t=e.message.parseHeader("Subscription-State");if(t&&t.state&&"terminated"===t.state){if(t.reason)switch(this.logger.log(`Terminated subscription with reason ${t.reason}`),t.reason){case"deactivated":case"timeout":return this.initSubscriberRequest(),void this.subscribe();case"probation":case"giveup":return this.initSubscriberRequest(),void(t.params&&t.params["retry-after"]?this.retryAfterTimer=setTimeout((()=>{this.subscribe()}),t.params["retry-after"]):this.subscribe())}this.unsubscribe()}}onRefresh(e){e.delegate={onAccept:e=>this.onAccepted(e)}}initSubscriberRequest(){const e={extraHeaders:this.extraHeaders,body:this.body?q(this.body):void 0};return this.subscriberRequest=new fe(this._userAgent.userAgentCore,this.targetURI,this.event,this.expires,e),this.subscriberRequest.delegate={onAccept:e=>this.onAccepted(e)},this.subscriberRequest}}class fe{constructor(e,t,s,i,r,n){this.core=e,this.target=t,this.event=s,this.expires=i,this.subscribed=!1,this.logger=e.loggerFactory.getLogger("sip.Subscriber"),this.delegate=n;const o="Allow: "+V.toString(),a=(r&&r.extraHeaders||[]).slice();a.push(o),a.push("Event: "+this.event),a.push("Expires: "+this.expires),a.push("Contact: "+this.core.configuration.contact.toString());const c=r&&r.body;this.message=e.makeOutgoingRequestMessage(G.SUBSCRIBE,this.target,this.core.configuration.aor,this.target,{},a,c)}dispose(){this.request&&(this.request.waitNotifyStop(),this.request.dispose(),this.request=void 0)}get state(){return this.subscription?this.subscription.subscriptionState:this.subscribed?re.NotifyWait:re.Initial}subscribe(){return this.subscribed?Promise.reject(new Error("Not in initial state. Did you call subscribe more than once?")):(this.subscribed=!0,new Promise((e=>{if(!this.message)throw new Error("Message undefined.");this.request=this.core.subscribe(this.message,{onAccept:e=>{this.delegate&&this.delegate.onAccept&&this.delegate.onAccept(e)},onNotify:t=>{this.subscription=t.subscription,this.subscription&&(this.subscription.autoRefresh=!0),e({success:t})},onNotifyTimeout:()=>{e({failure:{}})},onRedirect:t=>{e({failure:{response:t}})},onReject:t=>{e({failure:{response:t}})}})})))}}!function(e){e.Connecting="Connecting",e.Connected="Connected",e.Disconnecting="Disconnecting",e.Disconnected="Disconnected"}(oe=oe||(oe={})),function(e){e.Started="Started",e.Stopped="Stopped"}(ae=ae||(ae={}));class me{constructor(){this._dataLength=0,this._bufferLength=0,this._state=new Int32Array(4),this._buffer=new ArrayBuffer(68),this._buffer8=new Uint8Array(this._buffer,0,68),this._buffer32=new Uint32Array(this._buffer,0,17),this.start()}static hashStr(e,t=!1){return this.onePassHasher.start().appendStr(e).end(t)}static hashAsciiStr(e,t=!1){return this.onePassHasher.start().appendAsciiStr(e).end(t)}static _hex(e){const t=me.hexChars,s=me.hexOut;let i,r,n,o;for(o=0;o<4;o+=1)for(r=8*o,i=e[o],n=0;n<8;n+=2)s[r+1+n]=t.charAt(15&i),i>>>=4,s[r+0+n]=t.charAt(15&i),i>>>=4;return s.join("")}static _md5cycle(e,t){let s=e[0],i=e[1],r=e[2],n=e[3];s+=(i&r|~i&n)+t[0]-680876936|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[1]-389564586|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[2]+606105819|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[3]-1044525330|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[4]-176418897|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[5]+1200080426|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[6]-1473231341|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[7]-45705983|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[8]+1770035416|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[9]-1958414417|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[10]-42063|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[11]-1990404162|0,i=(i<<22|i>>>10)+r|0,s+=(i&r|~i&n)+t[12]+1804603682|0,s=(s<<7|s>>>25)+i|0,n+=(s&i|~s&r)+t[13]-40341101|0,n=(n<<12|n>>>20)+s|0,r+=(n&s|~n&i)+t[14]-1502002290|0,r=(r<<17|r>>>15)+n|0,i+=(r&n|~r&s)+t[15]+1236535329|0,i=(i<<22|i>>>10)+r|0,s+=(i&n|r&~n)+t[1]-165796510|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[6]-1069501632|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[11]+643717713|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[0]-373897302|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[5]-701558691|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[10]+38016083|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[15]-660478335|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[4]-405537848|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[9]+568446438|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[14]-1019803690|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[3]-187363961|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[8]+1163531501|0,i=(i<<20|i>>>12)+r|0,s+=(i&n|r&~n)+t[13]-1444681467|0,s=(s<<5|s>>>27)+i|0,n+=(s&r|i&~r)+t[2]-51403784|0,n=(n<<9|n>>>23)+s|0,r+=(n&i|s&~i)+t[7]+1735328473|0,r=(r<<14|r>>>18)+n|0,i+=(r&s|n&~s)+t[12]-1926607734|0,i=(i<<20|i>>>12)+r|0,s+=(i^r^n)+t[5]-378558|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[8]-2022574463|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[11]+1839030562|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[14]-35309556|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[1]-1530992060|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[4]+1272893353|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[7]-155497632|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[10]-1094730640|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[13]+681279174|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[0]-358537222|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[3]-722521979|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[6]+76029189|0,i=(i<<23|i>>>9)+r|0,s+=(i^r^n)+t[9]-640364487|0,s=(s<<4|s>>>28)+i|0,n+=(s^i^r)+t[12]-421815835|0,n=(n<<11|n>>>21)+s|0,r+=(n^s^i)+t[15]+530742520|0,r=(r<<16|r>>>16)+n|0,i+=(r^n^s)+t[2]-995338651|0,i=(i<<23|i>>>9)+r|0,s+=(r^(i|~n))+t[0]-198630844|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[7]+1126891415|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[14]-1416354905|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[5]-57434055|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[12]+1700485571|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[3]-1894986606|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[10]-1051523|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[1]-2054922799|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[8]+1873313359|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[15]-30611744|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[6]-1560198380|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[13]+1309151649|0,i=(i<<21|i>>>11)+r|0,s+=(r^(i|~n))+t[4]-145523070|0,s=(s<<6|s>>>26)+i|0,n+=(i^(s|~r))+t[11]-1120210379|0,n=(n<<10|n>>>22)+s|0,r+=(s^(n|~i))+t[2]+718787259|0,r=(r<<15|r>>>17)+n|0,i+=(n^(r|~s))+t[9]-343485551|0,i=(i<<21|i>>>11)+r|0,e[0]=s+e[0]|0,e[1]=i+e[1]|0,e[2]=r+e[2]|0,e[3]=n+e[3]|0}start(){return this._dataLength=0,this._bufferLength=0,this._state.set(me.stateIdentity),this}appendStr(e){const t=this._buffer8,s=this._buffer32;let i,r,n=this._bufferLength;for(r=0;r>>6),t[n++]=63&i|128;else if(i<55296||i>56319)t[n++]=224+(i>>>12),t[n++]=i>>>6&63|128,t[n++]=63&i|128;else{if(i=1024*(i-55296)+(e.charCodeAt(++r)-56320)+65536,i>1114111)throw new Error("Unicode standard supports code points up to U+10FFFF");t[n++]=240+(i>>>18),t[n++]=i>>>12&63|128,t[n++]=i>>>6&63|128,t[n++]=63&i|128}n>=64&&(this._dataLength+=64,me._md5cycle(this._state,s),n-=64,s[0]=s[16])}return this._bufferLength=n,this}appendAsciiStr(e){const t=this._buffer8,s=this._buffer32;let i,r=this._bufferLength,n=0;for(;;){for(i=Math.min(e.length-n,64-r);i--;)t[r++]=e.charCodeAt(n++);if(r<64)break;this._dataLength+=64,me._md5cycle(this._state,s),r=0}return this._bufferLength=r,this}appendByteArray(e){const t=this._buffer8,s=this._buffer32;let i,r=this._bufferLength,n=0;for(;;){for(i=Math.min(e.length-n,64-r);i--;)t[r++]=e[n++];if(r<64)break;this._dataLength+=64,me._md5cycle(this._state,s),r=0}return this._bufferLength=r,this}getState(){const e=this,t=e._state;return{buffer:String.fromCharCode.apply(null,e._buffer8),buflen:e._bufferLength,length:e._dataLength,state:[t[0],t[1],t[2],t[3]]}}setState(e){const t=e.buffer,s=e.state,i=this._state;let r;for(this._dataLength=e.length,this._bufferLength=e.buflen,i[0]=s[0],i[1]=s[1],i[2]=s[2],i[3]=s[3],r=0;r>2);let n;if(this._dataLength+=t,s[t]=128,s[t+1]=s[t+2]=s[t+3]=0,i.set(me.buffer32Identity.subarray(r),r),t>55&&(me._md5cycle(this._state,i),i.set(me.buffer32Identity)),n=8*this._dataLength,n<=4294967295)i[14]=n;else{const e=n.toString(16).match(/(.*?)(.{0,8})$/);if(null===e)return;const t=parseInt(e[2],16),s=parseInt(e[1],16)||0;i[14]=t,i[15]=s}return me._md5cycle(this._state,i),e?this._state:me._hex(this._state)}}function ve(e){return me.hashStr(e)}me.stateIdentity=new Int32Array([1732584193,-271733879,-1732584194,271733878]),me.buffer32Identity=new Int32Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]),me.hexChars="0123456789abcdef",me.hexOut=[],me.onePassHasher=new me,"5d41402abc4b2a76b9719d911017c592"!==me.hashStr("hello")&&console.error("Md5 self test failed.");class we{constructor(e,t,s,i){this.logger=e.getLogger("sipjs.digestauthentication"),this.username=s,this.password=i,this.ha1=t,this.nc=0,this.ncHex="00000000"}authenticate(e,t,s){if(this.algorithm=t.algorithm,this.realm=t.realm,this.nonce=t.nonce,this.opaque=t.opaque,this.stale=t.stale,this.algorithm){if("MD5"!==this.algorithm)return this.logger.warn("challenge with Digest algorithm different than 'MD5', authentication aborted"),!1}else this.algorithm="MD5";if(!this.realm)return this.logger.warn("challenge without Digest realm, authentication aborted"),!1;if(!this.nonce)return this.logger.warn("challenge without Digest nonce, authentication aborted"),!1;if(t.qop)if(t.qop.indexOf("auth")>-1)this.qop="auth";else{if(!(t.qop.indexOf("auth-int")>-1))return this.logger.warn("challenge without Digest qop different than 'auth' or 'auth-int', authentication aborted"),!1;this.qop="auth-int"}else this.qop=void 0;return this.method=e.method,this.uri=e.ruri,this.cnonce=$(12),this.nc+=1,this.updateNcHex(),4294967296===this.nc&&(this.nc=1,this.ncHex="00000001"),this.calculateResponse(s),!0}toString(){const e=[];if(!this.response)throw new Error("response field does not exist, cannot generate Authorization header");return e.push("algorithm="+this.algorithm),e.push('username="'+this.username+'"'),e.push('realm="'+this.realm+'"'),e.push('nonce="'+this.nonce+'"'),e.push('uri="'+this.uri+'"'),e.push('response="'+this.response+'"'),this.opaque&&e.push('opaque="'+this.opaque+'"'),this.qop&&(e.push("qop="+this.qop),e.push('cnonce="'+this.cnonce+'"'),e.push("nc="+this.ncHex)),"Digest "+e.join(", ")}updateNcHex(){const e=Number(this.nc).toString(16);this.ncHex="00000000".substr(0,8-e.length)+e}calculateResponse(e){let t,s;t=this.ha1,""!==t&&void 0!==t||(t=ve(this.username+":"+this.realm+":"+this.password)),"auth"===this.qop?(s=ve(this.method+":"+this.uri),this.response=ve(t+":"+this.nonce+":"+this.ncHex+":"+this.cnonce+":auth:"+s)):"auth-int"===this.qop?(s=ve(this.method+":"+this.uri+":"+ve(e||"")),this.response=ve(t+":"+this.nonce+":"+this.ncHex+":"+this.cnonce+":auth-int:"+s)):void 0===this.qop&&(s=ve(this.method+":"+this.uri),this.response=ve(t+":"+this.nonce+":"+s))}}!function(e){e[e.error=0]="error",e[e.warn=1]="warn",e[e.log=2]="log",e[e.debug=3]="debug"}(ce=ce||(ce={}));class be{constructor(e,t,s){this.logger=e,this.category=t,this.label=s}error(e){this.genericLog(ce.error,e)}warn(e){this.genericLog(ce.warn,e)}log(e){this.genericLog(ce.log,e)}debug(e){this.genericLog(ce.debug,e)}genericLog(e,t){this.logger.genericLog(e,this.category,this.label,t)}get level(){return this.logger.level}set level(e){this.logger.level=e}}class Te{constructor(){this.builtinEnabled=!0,this._level=ce.log,this.loggers={},this.logger=this.getLogger("sip:loggerfactory")}get level(){return this._level}set level(e){e>=0&&e<=3?this._level=e:e>3?this._level=3:ce.hasOwnProperty(e)?this._level=e:this.logger.error("invalid 'level' parameter value: "+JSON.stringify(e))}get connector(){return this._connector}set connector(e){e?"function"==typeof e?this._connector=e:this.logger.error("invalid 'connector' parameter value: "+JSON.stringify(e)):this._connector=void 0}getLogger(e,t){if(t&&3===this.level)return new be(this,e,t);if(this.loggers[e])return this.loggers[e];{const t=new be(this,e);return this.loggers[e]=t,t}}genericLog(e,t,s,i){this.level>=e&&this.builtinEnabled&&this.print(e,t,s,i),this.connector&&this.connector(ce[e],t,s,i)}print(e,t,s,i){if("string"==typeof i){const e=[new Date,t];s&&e.push(s),i=e.concat(i).join(" | ")}switch(e){case ce.error:console.error(i);break;case ce.warn:console.warn(i);break;case ce.log:console.log(i);break;case ce.debug:console.debug(i)}}}function Se(e,t){const s="\r\n";if(t.statusCode<100||t.statusCode>699)throw new TypeError("Invalid statusCode: "+t.statusCode);const i=t.reasonPhrase?t.reasonPhrase:I(t.statusCode);let r="SIP/2.0 "+t.statusCode+" "+i+s;t.statusCode>=100&&t.statusCode,t.statusCode;const n="From: "+e.getHeader("From")+s,o="Call-ID: "+e.callId+s,a="CSeq: "+e.cseq+" "+e.method+s,c=e.getHeaders("via").reduce(((e,t)=>e+"Via: "+t+s),"");let h="To: "+e.getHeader("to");if(t.statusCode>100&&!e.parseHeader("to").hasParam("tag")){let e=t.toTag;e||(e=C()),h+=";tag="+e}h+=s;let d="";t.supported&&(d="Supported: "+t.supported.join(", ")+s);let l="";t.userAgent&&(l="User-Agent: "+t.userAgent+s);let g="";return t.extraHeaders&&(g=t.extraHeaders.reduce(((e,t)=>e+t.trim()+s),"")),r+=c,r+=n,r+=h,r+=a,r+=o,r+=d,r+=l,r+=g,t.body?(r+="Content-Type: "+t.body.contentType+s,r+="Content-Length: "+D(t.body.content)+s+s,r+=t.body.content):r+="Content-Length: 0\r\n\r\n",{message:r}}!function(e){function t(e,t){let s=t,i=0,r=0;if(e.substring(s,s+2).match(/(^\r\n)/))return-2;for(;0===i;){if(r=e.indexOf("\r\n",s),-1===r)return r;!e.substring(r+2,r+4).match(/(^\r\n)/)&&e.charAt(r+2).match(/(^\s+)/)?s=r+2:i=r}return i}function s(e,t,s,i){const r=t.indexOf(":",s),n=t.substring(s,r).trim(),o=t.substring(r+1,i).trim();let a;switch(n.toLowerCase()){case"via":case"v":e.addHeader("via",o),1===e.getHeaders("via").length?(a=e.parseHeader("Via"),a&&(e.via=a,e.viaBranch=a.branch)):a=0;break;case"from":case"f":e.setHeader("from",o),a=e.parseHeader("from"),a&&(e.from=a,e.fromTag=a.getParam("tag"));break;case"to":case"t":e.setHeader("to",o),a=e.parseHeader("to"),a&&(e.to=a,e.toTag=a.getParam("tag"));break;case"record-route":if(a=y.parse(o,"Record_Route"),-1===a){a=void 0;break}if(!(a instanceof Array)){a=void 0;break}a.forEach((t=>{e.addHeader("record-route",o.substring(t.position,t.offset)),e.headers["Record-Route"][e.getHeaders("record-route").length-1].parsed=t.parsed}));break;case"call-id":case"i":e.setHeader("call-id",o),a=e.parseHeader("call-id"),a&&(e.callId=o);break;case"contact":case"m":if(a=y.parse(o,"Contact"),-1===a){a=void 0;break}if(!(a instanceof Array)){a=void 0;break}a.forEach((t=>{e.addHeader("contact",o.substring(t.position,t.offset)),e.headers.Contact[e.getHeaders("contact").length-1].parsed=t.parsed}));break;case"content-length":case"l":e.setHeader("content-length",o),a=e.parseHeader("content-length");break;case"content-type":case"c":e.setHeader("content-type",o),a=e.parseHeader("content-type");break;case"cseq":e.setHeader("cseq",o),a=e.parseHeader("cseq"),a&&(e.cseq=a.value),e instanceof x&&(e.method=a.method);break;case"max-forwards":e.setHeader("max-forwards",o),a=e.parseHeader("max-forwards");break;case"www-authenticate":e.setHeader("www-authenticate",o),a=e.parseHeader("www-authenticate");break;case"proxy-authenticate":e.setHeader("proxy-authenticate",o),a=e.parseHeader("proxy-authenticate");break;case"refer-to":case"r":e.setHeader("refer-to",o),a=e.parseHeader("refer-to"),a&&(e.referTo=a);break;default:e.addHeader(n.toLowerCase(),o),a=0}return void 0!==a||{error:"error parsing header '"+n+"'"}}e.getHeader=t,e.parseHeader=s,e.parseMessage=function(e,i){let r=0,n=e.indexOf("\r\n");if(-1===n)return void i.warn("no CRLF found, not a SIP message, discarded");const o=e.substring(0,n),a=y.parse(o,"Request_Response");let c,h;if(-1!==a){for(a.status_code?(c=new x,c.statusCode=a.status_code,c.reasonPhrase=a.reason_phrase):(c=new P,c.method=a.method,c.ruri=a.uri),c.data=e,r=n+2;;){if(n=t(e,r),-2===n){h=r+2;break}if(-1===n)return void i.error("malformed message");const o=s(c,e,r,n);if(o&&!0!==o)return void i.error(o.error);r=n+2}return c.hasHeader("content-length")?c.body=e.substr(h,Number(c.getHeader("content-length"))):c.body=e.substring(h),c}i.warn('error parsing first line of SIP message: "'+o+'"')}}(he=he||(he={}));class Re extends n{constructor(e){super(e||"Unspecified transport error.")}}class ye{constructor(e,t,s,i,r){this._transport=e,this._user=t,this._id=s,this._state=i,this.listeners=new Array,this.logger=t.loggerFactory.getLogger(r,s),this.logger.debug(`Constructing ${this.typeToString()} with id ${this.id}.`)}dispose(){this.logger.debug(`Destroyed ${this.typeToString()} with id ${this.id}.`)}get id(){return this._id}get kind(){throw new Error("Invalid kind.")}get state(){return this._state}get transport(){return this._transport}addStateChangeListener(e,t){const s=()=>{this.removeStateChangeListener(s),e()};!0===(null==t?void 0:t.once)?this.listeners.push(s):this.listeners.push(e)}notifyStateChangeListeners(){this.listeners.slice().forEach((e=>e()))}removeStateChangeListener(e){this.listeners=this.listeners.filter((t=>t!==e))}logTransportError(e,t){this.logger.error(e.message),this.logger.error(`Transport error occurred in ${this.typeToString()} with id ${this.id}.`),this.logger.error(t)}send(e){return this.transport.send(e).catch((e=>{if(e instanceof Re)throw this.onTransportError(e),e;let t;throw t=e&&"string"==typeof e.message?new Re(e.message):new Re,this.onTransportError(t),t}))}setState(e){this.logger.debug(`State change to "${e}" on ${this.typeToString()} with id ${this.id}.`),this._state=e,this._user.onStateChange&&this._user.onStateChange(e),this.notifyStateChangeListeners()}typeToString(){return"UnknownType"}}class Ee extends ye{constructor(e,t,s,i,r){super(t,s,e.viaBranch,i,r),this._request=e,this.user=s}get request(){return this._request}}!function(e){e.Accepted="Accepted",e.Calling="Calling",e.Completed="Completed",e.Confirmed="Confirmed",e.Proceeding="Proceeding",e.Terminated="Terminated",e.Trying="Trying"}(de=de||(de={}));class $e extends Ee{constructor(e,t,s){super(e,t,s,de.Proceeding,"sip.transaction.ist")}dispose(){this.stopProgressExtensionTimer(),this.H&&(clearTimeout(this.H),this.H=void 0),this.I&&(clearTimeout(this.I),this.I=void 0),this.L&&(clearTimeout(this.L),this.L=void 0),super.dispose()}get kind(){return"ist"}receiveRequest(e){switch(this.state){case de.Proceeding:if(e.method===G.INVITE)return void(this.lastProvisionalResponse&&this.send(this.lastProvisionalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")})));break;case de.Accepted:if(e.method===G.INVITE)return;break;case de.Completed:if(e.method===G.INVITE){if(!this.lastFinalResponse)throw new Error("Last final response undefined.");return void this.send(this.lastFinalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of final response.")}))}if(e.method===G.ACK)return void this.stateTransition(de.Confirmed);break;case de.Confirmed:case de.Terminated:if(e.method===G.INVITE||e.method===G.ACK)return;break;default:throw new Error(`Invalid state ${this.state}`)}const t=`INVITE server transaction received unexpected ${e.method} request while in state ${this.state}.`;this.logger.warn(t)}receiveResponse(e,t){if(e<100||e>699)throw new Error(`Invalid status code ${e}`);switch(this.state){case de.Proceeding:if(e>=100&&e<=199)return this.lastProvisionalResponse=t,e>100&&this.startProgressExtensionTimer(),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 1xx response.")}));if(e>=200&&e<=299)return this.lastFinalResponse=t,this.stateTransition(de.Accepted),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}));if(e>=300&&e<=699)return this.lastFinalResponse=t,this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send non-2xx final response.")}));break;case de.Accepted:if(e>=200&&e<=299)return void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}));break;case de.Completed:case de.Confirmed:case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`INVITE server transaction received unexpected ${e} response from TU while in state ${this.state}.`;throw this.logger.error(s),new Error(s)}retransmitAcceptedResponse(){this.state===de.Accepted&&this.lastFinalResponse&&this.send(this.lastFinalResponse).catch((e=>{this.logTransportError(e,"Failed to send 2xx response.")}))}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e)}typeToString(){return"INVITE server transaction"}stateTransition(e){const t=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Proceeding:t();break;case de.Accepted:case de.Completed:this.state!==de.Proceeding&&t();break;case de.Confirmed:this.state!==de.Completed&&t();break;case de.Terminated:this.state!==de.Accepted&&this.state!==de.Completed&&this.state!==de.Confirmed&&t();break;default:t()}this.stopProgressExtensionTimer(),e===de.Accepted&&(this.L=setTimeout((()=>this.timerL()),L.TIMER_L)),e===de.Completed&&(this.H=setTimeout((()=>this.timerH()),L.TIMER_H)),e===de.Confirmed&&(this.I=setTimeout((()=>this.timerI()),L.TIMER_I)),e===de.Terminated&&this.dispose(),this.setState(e)}startProgressExtensionTimer(){void 0===this.progressExtensionTimer&&(this.progressExtensionTimer=setInterval((()=>{if(this.logger.debug(`Progress extension timer expired for INVITE server transaction ${this.id}.`),!this.lastProvisionalResponse)throw new Error("Last provisional response undefined.");this.send(this.lastProvisionalResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")}))}),L.PROVISIONAL_RESPONSE_INTERVAL))}stopProgressExtensionTimer(){void 0!==this.progressExtensionTimer&&(clearInterval(this.progressExtensionTimer),this.progressExtensionTimer=void 0)}timerG(){}timerH(){this.logger.debug(`Timer H expired for INVITE server transaction ${this.id}.`),this.state===de.Completed&&(this.logger.warn("ACK to negative final response was never received, terminating transaction."),this.stateTransition(de.Terminated))}timerI(){this.logger.debug(`Timer I expired for INVITE server transaction ${this.id}.`),this.stateTransition(de.Terminated)}timerL(){this.logger.debug(`Timer L expired for INVITE server transaction ${this.id}.`),this.state===de.Accepted&&this.stateTransition(de.Terminated)}}class Ie extends ye{constructor(e,t,s,i,r){super(t,s,Ie.makeId(e),i,r),this._request=e,this.user=s,e.setViaHeader(this.id,t.protocol)}static makeId(e){if("CANCEL"===e.method){if(!e.branch)throw new Error("Outgoing CANCEL request without a branch.");return e.branch}return"z9hG4bK"+Math.floor(1e7*Math.random())}get request(){return this._request}onRequestTimeout(){this.user.onRequestTimeout&&this.user.onRequestTimeout()}}class Ce extends Ie{constructor(e,t,s){super(e,t,s,de.Trying,"sip.transaction.nict"),this.F=setTimeout((()=>this.timerF()),L.TIMER_F),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send initial outgoing request.")}))}dispose(){this.F&&(clearTimeout(this.F),this.F=void 0),this.K&&(clearTimeout(this.K),this.K=void 0),super.dispose()}get kind(){return"nict"}receiveResponse(e){const t=e.statusCode;if(!t||t<100||t>699)throw new Error(`Invalid status code ${t}`);switch(this.state){case de.Trying:if(t>=100&&t<=199)return this.stateTransition(de.Proceeding),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=699)return this.stateTransition(de.Completed),408===t?void this.onRequestTimeout():void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Proceeding:if(t>=100&&t<=199&&this.user.receiveResponse)return this.user.receiveResponse(e);if(t>=200&&t<=699)return this.stateTransition(de.Completed),408===t?void this.onRequestTimeout():void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Completed:case de.Terminated:return;default:throw new Error(`Invalid state ${this.state}`)}const s=`Non-INVITE client transaction received unexpected ${t} response while in state ${this.state}.`;this.logger.warn(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"non-INVITE client transaction"}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Trying:s();break;case de.Proceeding:this.state!==de.Trying&&s();break;case de.Completed:this.state!==de.Trying&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Trying&&this.state!==de.Proceeding&&this.state!==de.Completed&&(t||s());break;default:s()}e===de.Completed&&(this.F&&(clearTimeout(this.F),this.F=void 0),this.K=setTimeout((()=>this.timerK()),L.TIMER_K)),e===de.Terminated&&this.dispose(),this.setState(e)}timerF(){this.logger.debug(`Timer F expired for non-INVITE client transaction ${this.id}.`),this.state!==de.Trying&&this.state!==de.Proceeding||(this.onRequestTimeout(),this.stateTransition(de.Terminated))}timerK(){this.state===de.Completed&&this.stateTransition(de.Terminated)}}class Ae{constructor(e,t){this.core=e,this.dialogState=t,this.core.dialogs.set(this.id,this)}static initialDialogStateForUserAgentClient(e,t){const s=t.getHeaders("record-route").reverse(),i=t.parseHeader("contact");if(!i)throw new Error("Contact undefined.");if(!(i instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const r=i.uri,n=e.cseq,o=e.callId,a=e.fromTag,c=t.toTag;if(!o)throw new Error("Call id undefined.");if(!a)throw new Error("From tag undefined.");if(!c)throw new Error("To tag undefined.");if(!e.from)throw new Error("From undefined.");if(!e.to)throw new Error("To undefined.");const h=e.from.uri,d=e.to.uri;if(!t.statusCode)throw new Error("Incoming response status code undefined.");return{id:o+a+c,early:t.statusCode<200,callId:o,localTag:a,remoteTag:c,localSequenceNumber:n,remoteSequenceNumber:undefined,localURI:h,remoteURI:d,remoteTarget:r,routeSet:s,secure:!1}}static initialDialogStateForUserAgentServer(e,t,s=!1){const i=e.getHeaders("record-route"),r=e.parseHeader("contact");if(!r)throw new Error("Contact undefined.");if(!(r instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const n=r.uri,o=e.cseq,a=e.callId,c=t,h=e.fromTag,d=e.from.uri;return{id:a+c+h,early:s,callId:a,localTag:c,remoteTag:h,localSequenceNumber:undefined,remoteSequenceNumber:o,localURI:e.to.uri,remoteURI:d,remoteTarget:n,routeSet:i,secure:!1}}dispose(){this.core.dialogs.delete(this.id)}get id(){return this.dialogState.id}get early(){return this.dialogState.early}get callId(){return this.dialogState.callId}get localTag(){return this.dialogState.localTag}get remoteTag(){return this.dialogState.remoteTag}get localSequenceNumber(){return this.dialogState.localSequenceNumber}get remoteSequenceNumber(){return this.dialogState.remoteSequenceNumber}get localURI(){return this.dialogState.localURI}get remoteURI(){return this.dialogState.remoteURI}get remoteTarget(){return this.dialogState.remoteTarget}get routeSet(){return this.dialogState.routeSet}get secure(){return this.dialogState.secure}get userAgentCore(){return this.core}confirm(){this.dialogState.early=!1}receiveRequest(e){if(e.method!==G.ACK){if(this.remoteSequenceNumber){if(e.cseq<=this.remoteSequenceNumber)throw new Error("Out of sequence in dialog request. Did you forget to call sequenceGuard()?");this.dialogState.remoteSequenceNumber=e.cseq}this.remoteSequenceNumber||(this.dialogState.remoteSequenceNumber=e.cseq)}}recomputeRouteSet(e){this.dialogState.routeSet=e.getHeaders("record-route").reverse()}createOutgoingRequestMessage(e,t){const s=this.remoteURI,i=this.remoteTag,r=this.localURI,n=this.localTag,o=this.callId;let a;a=t&&t.cseq?t.cseq:this.dialogState.localSequenceNumber?this.dialogState.localSequenceNumber+=1:this.dialogState.localSequenceNumber=1;const c=this.remoteTarget,h=this.routeSet,d=t&&t.extraHeaders,l=t&&t.body;return this.userAgentCore.makeOutgoingRequestMessage(e,c,r,s,{callId:o,cseq:a,fromTag:n,toTag:i,routeSet:h},d,l)}incrementLocalSequenceNumber(){if(!this.dialogState.localSequenceNumber)throw new Error("Local sequence number undefined.");this.dialogState.localSequenceNumber+=1}sequenceGuard(e){return e.method===G.ACK||(!(this.remoteSequenceNumber&&e.cseq<=this.remoteSequenceNumber)||(this.core.replyStateless(e,{statusCode:500}),!1))}}class De extends Ie{constructor(e,t,s){super(e,t,s,de.Calling,"sip.transaction.ict"),this.ackRetransmissionCache=new Map,this.B=setTimeout((()=>this.timerB()),L.TIMER_B),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send initial outgoing request.")}))}dispose(){this.B&&(clearTimeout(this.B),this.B=void 0),this.D&&(clearTimeout(this.D),this.D=void 0),this.M&&(clearTimeout(this.M),this.M=void 0),super.dispose()}get kind(){return"ict"}ackResponse(e){const t=e.toTag;if(!t)throw new Error("To tag undefined.");const s="z9hG4bK"+Math.floor(1e7*Math.random());e.setViaHeader(s,this.transport.protocol),this.ackRetransmissionCache.set(t,e),this.send(e.toString()).catch((e=>{this.logTransportError(e,"Failed to send ACK to 2xx response.")}))}receiveResponse(e){const t=e.statusCode;if(!t||t<100||t>699)throw new Error(`Invalid status code ${t}`);switch(this.state){case de.Calling:if(t>=100&&t<=199)return this.stateTransition(de.Proceeding),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=299)return this.ackRetransmissionCache.set(e.toTag,void 0),this.stateTransition(de.Accepted),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=300&&t<=699)return this.stateTransition(de.Completed),this.ack(e),void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Proceeding:if(t>=100&&t<=199)return void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=200&&t<=299)return this.ackRetransmissionCache.set(e.toTag,void 0),this.stateTransition(de.Accepted),void(this.user.receiveResponse&&this.user.receiveResponse(e));if(t>=300&&t<=699)return this.stateTransition(de.Completed),this.ack(e),void(this.user.receiveResponse&&this.user.receiveResponse(e));break;case de.Accepted:if(t>=200&&t<=299){if(!this.ackRetransmissionCache.has(e.toTag))return this.ackRetransmissionCache.set(e.toTag,void 0),void(this.user.receiveResponse&&this.user.receiveResponse(e));const t=this.ackRetransmissionCache.get(e.toTag);return t?void this.send(t.toString()).catch((e=>{this.logTransportError(e,"Failed to send retransmission of ACK to 2xx response.")})):void 0}break;case de.Completed:if(t>=300&&t<=699)return void this.ack(e);break;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`Received unexpected ${t} response while in state ${this.state}.`;this.logger.warn(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"INVITE client transaction"}ack(e){const t=this.request.ruri,s=this.request.callId,i=this.request.cseq,r=this.request.getHeader("from"),n=e.getHeader("to"),o=this.request.getHeader("via"),a=this.request.getHeader("route");if(!r)throw new Error("From undefined.");if(!n)throw new Error("To undefined.");if(!o)throw new Error("Via undefined.");let c=`ACK ${t} SIP/2.0\r\n`;a&&(c+=`Route: ${a}\r\n`),c+=`Via: ${o}\r\n`,c+=`To: ${n}\r\n`,c+=`From: ${r}\r\n`,c+=`Call-ID: ${s}\r\n`,c+=`CSeq: ${i} ACK\r\n`,c+="Max-Forwards: 70\r\n",c+="Content-Length: 0\r\n\r\n",this.send(c).catch((e=>{this.logTransportError(e,"Failed to send ACK to non-2xx response.")}))}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Calling:s();break;case de.Proceeding:this.state!==de.Calling&&s();break;case de.Accepted:case de.Completed:this.state!==de.Calling&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Calling&&this.state!==de.Accepted&&this.state!==de.Completed&&(t||s());break;default:s()}this.B&&(clearTimeout(this.B),this.B=void 0),de.Proceeding,e===de.Completed&&(this.D=setTimeout((()=>this.timerD()),L.TIMER_D)),e===de.Accepted&&(this.M=setTimeout((()=>this.timerM()),L.TIMER_M)),e===de.Terminated&&this.dispose(),this.setState(e)}timerA(){}timerB(){this.logger.debug(`Timer B expired for INVITE client transaction ${this.id}.`),this.state===de.Calling&&(this.onRequestTimeout(),this.stateTransition(de.Terminated))}timerD(){this.logger.debug(`Timer D expired for INVITE client transaction ${this.id}.`),this.state===de.Completed&&this.stateTransition(de.Terminated)}timerM(){this.logger.debug(`Timer M expired for INVITE client transaction ${this.id}.`),this.state===de.Accepted&&this.stateTransition(de.Terminated)}}class He{constructor(e,t,s,i){this.transactionConstructor=e,this.core=t,this.message=s,this.delegate=i,this.challenged=!1,this.stale=!1,this.logger=this.loggerFactory.getLogger("sip.user-agent-client"),this.init()}dispose(){this.transaction.dispose()}get loggerFactory(){return this.core.loggerFactory}get transaction(){if(!this._transaction)throw new Error("Transaction undefined.");return this._transaction}cancel(e,t={}){if(!this.transaction)throw new Error("Transaction undefined.");if(!this.message.to)throw new Error("To undefined.");if(!this.message.from)throw new Error("From undefined.");const s=this.core.makeOutgoingRequestMessage(G.CANCEL,this.message.ruri,this.message.from.uri,this.message.to.uri,{toTag:this.message.toTag,fromTag:this.message.fromTag,callId:this.message.callId,cseq:this.message.cseq},t.extraHeaders);return s.branch=this.message.branch,this.message.headers.Route&&(s.headers.Route=this.message.headers.Route),e&&s.setHeader("Reason",e),this.transaction.state===de.Proceeding?new He(Ce,this.core,s):this.transaction.addStateChangeListener((()=>{this.transaction&&this.transaction.state===de.Proceeding&&new He(Ce,this.core,s)}),{once:!0}),s}authenticationGuard(e,t){const s=e.statusCode;if(!s)throw new Error("Response status code undefined.");if(401!==s&&407!==s)return!0;let i,r;if(401===s?(i=e.parseHeader("www-authenticate"),r="authorization"):(i=e.parseHeader("proxy-authenticate"),r="proxy-authorization"),!i)return this.logger.warn(s+" with wrong or missing challenge, cannot authenticate"),!0;if(this.challenged&&(this.stale||!0!==i.stale))return this.logger.warn(s+" apparently in authentication loop, cannot authenticate"),!0;if(!this.credentials&&(this.credentials=this.core.configuration.authenticationFactory(),!this.credentials))return this.logger.warn("Unable to obtain credentials, cannot authenticate"),!0;if(!this.credentials.authenticate(this.message,i))return!0;this.challenged=!0,i.stale&&(this.stale=!0);let n=this.message.cseq+=1;return t&&t.localSequenceNumber&&(t.incrementLocalSequenceNumber(),n=this.message.cseq=t.localSequenceNumber),this.message.setHeader("cseq",n+" "+this.message.method),this.message.setHeader(r,this.credentials.toString()),this.init(),!1}onRequestTimeout(){this.logger.warn("User agent client request timed out. Generating internal 408 Request Timeout.");const e=new x;e.statusCode=408,e.reasonPhrase="Request Timeout",this.receiveResponse(e)}onTransportError(e){this.logger.error(e.message),this.logger.error("User agent client request transport error. Generating internal 503 Service Unavailable.");const t=new x;t.statusCode=503,t.reasonPhrase="Service Unavailable",this.receiveResponse(t)}receiveResponse(e){if(!this.authenticationGuard(e))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e});break;case/^1[0-9]{2}$/.test(t):this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e});break;case/^2[0-9]{2}$/.test(t):this.delegate&&this.delegate.onAccept&&this.delegate.onAccept({message:e});break;case/^3[0-9]{2}$/.test(t):this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e});break;case/^[4-6][0-9]{2}$/.test(t):this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e});break;default:throw new Error(`Invalid status code ${t}`)}}init(){const e={loggerFactory:this.loggerFactory,onRequestTimeout:()=>this.onRequestTimeout(),onStateChange:e=>{e===de.Terminated&&(this.core.userAgentClients.delete(s),t===this._transaction&&this.dispose())},onTransportError:e=>this.onTransportError(e),receiveResponse:e=>this.receiveResponse(e)},t=new this.transactionConstructor(this.message,this.core.transport,e);this._transaction=t;const s=t.id+t.request.method;this.core.userAgentClients.set(s,this)}}class Pe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.BYE,s);super(Ce,e.userAgentCore,i,t),e.dispose()}}class xe extends Ee{constructor(e,t,s){super(e,t,s,de.Trying,"sip.transaction.nist")}dispose(){this.J&&(clearTimeout(this.J),this.J=void 0),super.dispose()}get kind(){return"nist"}receiveRequest(e){switch(this.state){case de.Trying:break;case de.Proceeding:if(!this.lastResponse)throw new Error("Last response undefined.");this.send(this.lastResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of provisional response.")}));break;case de.Completed:if(!this.lastResponse)throw new Error("Last response undefined.");this.send(this.lastResponse).catch((e=>{this.logTransportError(e,"Failed to send retransmission of final response.")}));break;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}}receiveResponse(e,t){if(e<100||e>699)throw new Error(`Invalid status code ${e}`);if(e>100&&e<=199)throw new Error("Provisional response other than 100 not allowed.");switch(this.state){case de.Trying:if(this.lastResponse=t,e>=100&&e<200)return this.stateTransition(de.Proceeding),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send provisional response.")}));if(e>=200&&e<=699)return this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send final response.")}));break;case de.Proceeding:if(this.lastResponse=t,e>=200&&e<=699)return this.stateTransition(de.Completed),void this.send(t).catch((e=>{this.logTransportError(e,"Failed to send final response.")}));break;case de.Completed:return;case de.Terminated:break;default:throw new Error(`Invalid state ${this.state}`)}const s=`Non-INVITE server transaction received unexpected ${e} response from TU while in state ${this.state}.`;throw this.logger.error(s),new Error(s)}onTransportError(e){this.user.onTransportError&&this.user.onTransportError(e),this.stateTransition(de.Terminated,!0)}typeToString(){return"non-INVITE server transaction"}stateTransition(e,t=!1){const s=()=>{throw new Error(`Invalid state transition from ${this.state} to ${e}`)};switch(e){case de.Trying:s();break;case de.Proceeding:this.state!==de.Trying&&s();break;case de.Completed:this.state!==de.Trying&&this.state!==de.Proceeding&&s();break;case de.Terminated:this.state!==de.Proceeding&&this.state!==de.Completed&&(t||s());break;default:s()}e===de.Completed&&(this.J=setTimeout((()=>this.timerJ()),L.TIMER_J)),e===de.Terminated&&this.dispose(),this.setState(e)}timerJ(){this.logger.debug(`Timer J expired for NON-INVITE server transaction ${this.id}.`),this.state===de.Completed&&this.stateTransition(de.Terminated)}}class ke{constructor(e,t,s,i){this.transactionConstructor=e,this.core=t,this.message=s,this.delegate=i,this.logger=this.loggerFactory.getLogger("sip.user-agent-server"),this.toTag=s.toTag?s.toTag:C(),this.init()}dispose(){this.transaction.dispose()}get loggerFactory(){return this.core.loggerFactory}get transaction(){if(!this._transaction)throw new Error("Transaction undefined.");return this._transaction}accept(e={statusCode:200}){if(!this.acceptable)throw new B(`${this.message.method} not acceptable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<200||t>299)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}progress(e={statusCode:180}){if(!this.progressable)throw new B(`${this.message.method} not progressable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<101||t>199)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}redirect(e,t={statusCode:302}){if(!this.redirectable)throw new B(`${this.message.method} not redirectable in state ${this.transaction.state}.`);const s=t.statusCode;if(s<300||s>399)throw new TypeError(`Invalid statusCode: ${s}`);const i=new Array;e.forEach((e=>i.push(`Contact: ${e.toString()}`))),t.extraHeaders=(t.extraHeaders||[]).concat(i);return this.reply(t)}reject(e={statusCode:480}){if(!this.rejectable)throw new B(`${this.message.method} not rejectable in state ${this.transaction.state}.`);const t=e.statusCode;if(t<400||t>699)throw new TypeError(`Invalid statusCode: ${t}`);return this.reply(e)}trying(e){if(!this.tryingable)throw new B(`${this.message.method} not tryingable in state ${this.transaction.state}.`);return this.reply({statusCode:100})}receiveCancel(e){this.delegate&&this.delegate.onCancel&&this.delegate.onCancel(e)}get acceptable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding||this.transaction.state===de.Accepted;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get progressable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return!1;throw new Error("Unknown transaction type.")}get redirectable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get rejectable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying||this.transaction.state===de.Proceeding;throw new Error("Unknown transaction type.")}get tryingable(){if(this.transaction instanceof $e)return this.transaction.state===de.Proceeding;if(this.transaction instanceof xe)return this.transaction.state===de.Trying;throw new Error("Unknown transaction type.")}reply(e){e.toTag||100===e.statusCode||(e.toTag=this.toTag),e.userAgent=e.userAgent||this.core.configuration.userAgentHeaderFieldValue,e.supported=e.supported||this.core.configuration.supportedOptionTagsResponse;const t=Se(this.message,e);return this.transaction.receiveResponse(e.statusCode,t.message),t}init(){const e={loggerFactory:this.loggerFactory,onStateChange:e=>{e===de.Terminated&&(this.core.userAgentServers.delete(s),this.dispose())},onTransportError:e=>{this.logger.error(e.message),this.delegate&&this.delegate.onTransportError?this.delegate.onTransportError(e):this.logger.error("User agent server response transport error.")}},t=new this.transactionConstructor(this.message,this.core.transport,e);this._transaction=t;const s=t.id;this.core.userAgentServers.set(t.id,this)}}class _e extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}class qe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.INFO,s);super(Ce,e.userAgentCore,i,t)}}class Me extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}class Ne extends He{constructor(e,t,s){super(Ce,e,t,s)}}class Oe extends ke{constructor(e,t,s){super(xe,e,t,s)}}class Ue extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.NOTIFY,s);super(Ce,e.userAgentCore,i,t)}}class je extends ke{constructor(e,t,s){const i=void 0!==e.userAgentCore?e.userAgentCore:e;super(xe,i,t,s)}}class Fe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.PRACK,s);super(Ce,e.userAgentCore,i,t),e.signalingStateTransition(i)}}class Le extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s),e.signalingStateTransition(t),this.dialog=e}accept(e={statusCode:200}){return e.body&&this.dialog.signalingStateTransition(e.body),super.accept(e)}}class Be extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.INVITE,s);super(De,e.userAgentCore,i,t),this.delegate=t,e.signalingStateTransition(i),e.reinviteUserAgentClient=this,this.dialog=e}receiveResponse(e){if(!this.authenticationGuard(e,this.dialog))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e});break;case/^1[0-9]{2}$/.test(t):this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e,session:this.dialog,prack:e=>{throw new Error("Unimplemented.")}});break;case/^2[0-9]{2}$/.test(t):this.dialog.signalingStateTransition(e),this.delegate&&this.delegate.onAccept&&this.delegate.onAccept({message:e,session:this.dialog,ack:e=>this.dialog.ack(e)});break;case/^3[0-9]{2}$/.test(t):this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentClient=void 0,this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e});break;case/^[4-6][0-9]{2}$/.test(t):this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentClient=void 0,this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e});break;default:throw new Error(`Invalid status code ${t}`)}}}class Ge extends ke{constructor(e,t,s){super($e,e.userAgentCore,t,s),e.reinviteUserAgentServer=this,this.dialog=e}accept(e={statusCode:200}){e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(this.dialog.routeSet.map((e=>`Record-Route: ${e}`)));const t=super.accept(e),s=this.dialog,i=Object.assign(Object.assign({},t),{session:s});return e.body&&this.dialog.signalingStateTransition(e.body),this.dialog.reConfirm(),i}progress(e={statusCode:180}){const t=super.progress(e),s=this.dialog,i=Object.assign(Object.assign({},t),{session:s});return e.body&&this.dialog.signalingStateTransition(e.body),i}redirect(e,t={statusCode:302}){throw this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentServer=void 0,new Error("Unimplemented.")}reject(e={statusCode:488}){return this.dialog.signalingStateRollback(),this.dialog.reinviteUserAgentServer=void 0,super.reject(e)}}class Ve extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.REFER,s);super(Ce,e.userAgentCore,i,t)}}class We extends ke{constructor(e,t,s){const i=void 0!==e.userAgentCore?e.userAgentCore:e;super(xe,i,t,s)}}class Ke extends Ae{constructor(e,t,s,i){super(t,s),this.initialTransaction=e,this._signalingState=U.Initial,this.ackWait=!1,this.ackProcessing=!1,this.delegate=i,e instanceof $e&&(this.ackWait=!0),this.early||this.start2xxRetransmissionTimer(),this.signalingStateTransition(e.request),this.logger=t.loggerFactory.getLogger("sip.invite-dialog"),this.logger.log(`INVITE dialog ${this.id} constructed`)}dispose(){super.dispose(),this._signalingState=U.Closed,this._offer=void 0,this._answer=void 0,this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.logger.log(`INVITE dialog ${this.id} destroyed`)}get sessionState(){return this.early?O.Early:this.ackWait?O.AckWait:this._signalingState===U.Closed?O.Terminated:O.Confirmed}get signalingState(){return this._signalingState}get offer(){return this._offer}get answer(){return this._answer}confirm(){this.early&&this.start2xxRetransmissionTimer(),super.confirm()}reConfirm(){this.reinviteUserAgentServer&&this.startReInvite2xxRetransmissionTimer()}ack(e={}){let t;if(this.logger.log(`INVITE dialog ${this.id} sending ACK request`),this.reinviteUserAgentClient){if(!(this.reinviteUserAgentClient.transaction instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");t=this.reinviteUserAgentClient.transaction,this.reinviteUserAgentClient=void 0}else{if(!(this.initialTransaction instanceof De))throw new Error("Initial transaction not instance of InviteClientTransaction.");t=this.initialTransaction}const s=this.createOutgoingRequestMessage(G.ACK,{cseq:t.request.cseq,extraHeaders:e.extraHeaders,body:e.body});return t.ackResponse(s),this.signalingStateTransition(s),{message:s}}bye(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending BYE request`),this.initialTransaction instanceof $e){if(this.early)throw new Error("UAS MUST NOT send a BYE on early dialogs.");if(this.ackWait&&this.initialTransaction.state!==de.Terminated)throw new Error("UAS MUST NOT send a BYE on a confirmed dialog until it has received an ACK for its 2xx response or until the server transaction times out.")}return new Pe(this,e,t)}info(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending INFO request`),this.early)throw new Error("Dialog not confirmed.");return new qe(this,e,t)}invite(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending INVITE request`),this.early)throw new Error("Dialog not confirmed.");if(this.reinviteUserAgentClient)throw new Error("There is an ongoing re-INVITE client transaction.");if(this.reinviteUserAgentServer)throw new Error("There is an ongoing re-INVITE server transaction.");return new Be(this,e,t)}message(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending MESSAGE request`),this.early)throw new Error("Dialog not confirmed.");const s=this.createOutgoingRequestMessage(G.MESSAGE,t);return new Ne(this.core,s,e)}notify(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending NOTIFY request`),this.early)throw new Error("Dialog not confirmed.");return new Ue(this,e,t)}prack(e,t){return this.logger.log(`INVITE dialog ${this.id} sending PRACK request`),new Fe(this,e,t)}refer(e,t){if(this.logger.log(`INVITE dialog ${this.id} sending REFER request`),this.early)throw new Error("Dialog not confirmed.");return new Ve(this,e,t)}receiveRequest(e){if(this.logger.log(`INVITE dialog ${this.id} received ${e.method} request`),e.method!==G.ACK)if(this.sequenceGuard(e)){if(super.receiveRequest(e),e.method===G.INVITE){const t=()=>{const e=this.ackWait?"waiting for initial ACK":"processing initial ACK";this.logger.warn(`INVITE dialog ${this.id} received re-INVITE while ${e}`);let t="RFC 5407 suggests the following to avoid this race condition... ";t+=" Note: Implementation issues are outside the scope of this document,",t+=" but the following tip is provided for avoiding race conditions of",t+=" this type. The caller can delay sending re-INVITE F6 for some period",t+=" of time (2 seconds, perhaps), after which the caller can reasonably",t+=" assume that its ACK has been received. Implementors can decouple the",t+=" actions of the user (e.g., pressing the hold button) from the actions",t+=" of the protocol (the sending of re-INVITE F6), so that the UA can",t+=" behave like this. In this case, it is the implementor's choice as to",t+=" how long to wait. In most cases, such an implementation may be",t+=" useful to prevent the type of race condition shown in this section.",t+=" This document expresses no preference about whether or not they",t+=" should wait for an ACK to be delivered. After considering the impact",t+=" on user experience, implementors should decide whether or not to wait",t+=" for a while, because the user experience depends on the",t+=" implementation and has no direct bearing on protocol behavior.",this.logger.warn("RFC 5407 suggests the following to avoid this race condition... Note: Implementation issues are outside the scope of this document, but the following tip is provided for avoiding race conditions of this type. The caller can delay sending re-INVITE F6 for some period of time (2 seconds, perhaps), after which the caller can reasonably assume that its ACK has been received. Implementors can decouple the actions of the user (e.g., pressing the hold button) from the actions of the protocol (the sending of re-INVITE F6), so that the UA can behave like this. In this case, it is the implementor's choice as to how long to wait. In most cases, such an implementation may be useful to prevent the type of race condition shown in this section. This document expresses no preference about whether or not they should wait for an ACK to be delivered. After considering the impact on user experience, implementors should decide whether or not to wait for a while, because the user experience depends on the implementation and has no direct bearing on protocol behavior.")},s=[`Retry-After: ${Math.floor(10*Math.random())+1}`];if(this.ackProcessing)return this.core.replyStateless(e,{statusCode:500,extraHeaders:s}),void t();if(this.ackWait&&this.signalingState!==U.Stable)return this.core.replyStateless(e,{statusCode:500,extraHeaders:s}),void t();if(this.reinviteUserAgentServer)return void this.core.replyStateless(e,{statusCode:500,extraHeaders:s});if(this.reinviteUserAgentClient)return void this.core.replyStateless(e,{statusCode:491})}if(e.method===G.INVITE){const t=e.parseHeader("contact");if(!t)throw new Error("Contact undefined.");if(!(t instanceof v))throw new Error("Contact not instance of NameAddrHeader.");this.dialogState.remoteTarget=t.uri}switch(e.method){case G.BYE:{const t=new _e(this,e);this.delegate&&this.delegate.onBye?this.delegate.onBye(t):t.accept(),this.dispose()}break;case G.INFO:{const t=new Me(this,e);this.delegate&&this.delegate.onInfo?this.delegate.onInfo(t):t.reject({statusCode:469,extraHeaders:["Recv-Info:"]})}break;case G.INVITE:{const t=new Ge(this,e);this.signalingStateTransition(e),this.delegate&&this.delegate.onInvite?this.delegate.onInvite(t):t.reject({statusCode:488})}break;case G.MESSAGE:{const t=new Oe(this.core,e);this.delegate&&this.delegate.onMessage?this.delegate.onMessage(t):t.accept()}break;case G.NOTIFY:{const t=new je(this,e);this.delegate&&this.delegate.onNotify?this.delegate.onNotify(t):t.accept()}break;case G.PRACK:{const t=new Le(this,e);this.delegate&&this.delegate.onPrack?this.delegate.onPrack(t):t.accept()}break;case G.REFER:{const t=new We(this,e);this.delegate&&this.delegate.onRefer?this.delegate.onRefer(t):t.reject()}break;default:this.logger.log(`INVITE dialog ${this.id} received unimplemented ${e.method} request`),this.core.replyStateless(e,{statusCode:501})}}else this.logger.log(`INVITE dialog ${this.id} rejected out of order ${e.method} request.`);else{if(this.ackWait){if(this.initialTransaction instanceof De)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);if(this.initialTransaction.request.cseq!==e.cseq)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);this.ackWait=!1}else{if(!this.reinviteUserAgentServer)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);if(this.reinviteUserAgentServer.transaction.request.cseq!==e.cseq)return void this.logger.warn(`INVITE dialog ${this.id} received unexpected ${e.method} request, dropping.`);this.reinviteUserAgentServer=void 0}if(this.signalingStateTransition(e),this.delegate&&this.delegate.onAck){const t=this.delegate.onAck({message:e});t instanceof Promise&&(this.ackProcessing=!0,t.then((()=>this.ackProcessing=!1)).catch((()=>this.ackProcessing=!1)))}}}reliableSequenceGuard(e){const t=e.statusCode;if(!t)throw new Error("Status code undefined");if(t>100&&t<200){const t=e.getHeader("require"),s=e.getHeader("rseq"),i=t&&t.includes("100rel")&&s?Number(s):void 0;if(i){if(this.rseq&&this.rseq+1!==i)return!1;this.rseq=this.rseq?this.rseq+1:i}}return!0}signalingStateRollback(){this._signalingState!==U.HaveLocalOffer&&this.signalingState!==U.HaveRemoteOffer||this._rollbackOffer&&this._rollbackAnswer&&(this._signalingState=U.Stable,this._offer=this._rollbackOffer,this._answer=this._rollbackAnswer)}signalingStateTransition(e){const t=N(e);if(t&&"session"===t.contentDisposition){if(this._signalingState===U.Stable&&(this._rollbackOffer=this._offer,this._rollbackAnswer=this._answer),e instanceof P)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveRemoteOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:this._signalingState=U.Stable,this._answer=t;break;case U.HaveRemoteOffer:case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(e instanceof x)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveRemoteOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:this._signalingState=U.Stable,this._answer=t;break;case U.HaveRemoteOffer:case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(e instanceof k)switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveLocalOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:break;case U.HaveRemoteOffer:this._signalingState=U.Stable,this._answer=t;break;case U.Closed:break;default:throw new Error("Unexpected signaling state.")}if(M(e))switch(this._signalingState){case U.Initial:case U.Stable:this._signalingState=U.HaveLocalOffer,this._offer=t,this._answer=void 0;break;case U.HaveLocalOffer:break;case U.HaveRemoteOffer:this._signalingState=U.Stable,this._answer=t;break;case U.Closed:break;default:throw new Error("Unexpected signaling state.")}}}start2xxRetransmissionTimer(){if(this.initialTransaction instanceof $e){const e=this.initialTransaction;let t=L.T1;const s=()=>{this.ackWait?(this.logger.log("No ACK for 2xx response received, attempting retransmission"),e.retransmitAcceptedResponse(),t=Math.min(2*t,L.T2),this.invite2xxTimer=setTimeout(s,t)):this.invite2xxTimer=void 0};this.invite2xxTimer=setTimeout(s,t);const i=()=>{e.state===de.Terminated&&(e.removeStateChangeListener(i),this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.ackWait&&(this.delegate&&this.delegate.onAckTimeout?this.delegate.onAckTimeout():this.bye()))};e.addStateChangeListener(i)}}startReInvite2xxRetransmissionTimer(){if(this.reinviteUserAgentServer&&this.reinviteUserAgentServer.transaction instanceof $e){const e=this.reinviteUserAgentServer.transaction;let t=L.T1;const s=()=>{this.reinviteUserAgentServer?(this.logger.log("No ACK for 2xx response received, attempting retransmission"),e.retransmitAcceptedResponse(),t=Math.min(2*t,L.T2),this.invite2xxTimer=setTimeout(s,t)):this.invite2xxTimer=void 0};this.invite2xxTimer=setTimeout(s,t);const i=()=>{e.state===de.Terminated&&(e.removeStateChangeListener(i),this.invite2xxTimer&&(clearTimeout(this.invite2xxTimer),this.invite2xxTimer=void 0),this.reinviteUserAgentServer)};e.addStateChangeListener(i)}}}class Ye extends He{constructor(e,t,s){super(De,e,t,s),this.confirmedDialogAcks=new Map,this.confirmedDialogs=new Map,this.earlyDialogs=new Map,this.delegate=s}dispose(){this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),super.dispose()}onTransportError(e){if(this.transaction.state===de.Calling)return super.onTransportError(e);this.logger.error(e.message),this.logger.error("User agent client request transport error while sending ACK.")}receiveResponse(e){if(!this.authenticationGuard(e))return;const t=e.statusCode?e.statusCode.toString():"";if(!t)throw new Error("Response status code undefined.");switch(!0){case/^100$/.test(t):return void(this.delegate&&this.delegate.onTrying&&this.delegate.onTrying({message:e}));case/^1[0-9]{2}$/.test(t):{if(!e.toTag)return void this.logger.warn("Non-100 1xx INVITE response received without a to tag, dropping.");if(!e.parseHeader("contact"))return void this.logger.error("Non-100 1xx INVITE response received without a Contact header field, dropping.");const t=Ae.initialDialogStateForUserAgentClient(this.message,e);let s=this.earlyDialogs.get(t.id);if(!s){const e=this.transaction;if(!(e instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");s=new Ke(e,this.core,t),this.earlyDialogs.set(s.id,s)}if(!s.reliableSequenceGuard(e))return void this.logger.warn("1xx INVITE reliable response received out of order or is a retransmission, dropping.");s.signalingState!==U.Initial&&s.signalingState!==U.HaveLocalOffer||s.signalingStateTransition(e);const i=s;this.delegate&&this.delegate.onProgress&&this.delegate.onProgress({message:e,session:i,prack:e=>i.prack(void 0,e)})}return;case/^2[0-9]{2}$/.test(t):{if(!e.toTag)return void this.logger.error("2xx INVITE response received without a to tag, dropping.");if(!e.parseHeader("contact"))return void this.logger.error("2xx INVITE response received without a Contact header field, dropping.");const t=Ae.initialDialogStateForUserAgentClient(this.message,e);let s=this.confirmedDialogs.get(t.id);if(s){const e=this.confirmedDialogAcks.get(t.id);if(e){const t=this.transaction;if(!(t instanceof De))throw new Error("Client transaction not instance of InviteClientTransaction.");t.ackResponse(e.message)}return}if(s=this.earlyDialogs.get(t.id),s)s.confirm(),s.recomputeRouteSet(e),this.earlyDialogs.delete(s.id),this.confirmedDialogs.set(s.id,s);else{const e=this.transaction;if(!(e instanceof De))throw new Error("Transaction not instance of InviteClientTransaction.");s=new Ke(e,this.core,t),this.confirmedDialogs.set(s.id,s)}s.signalingState!==U.Initial&&s.signalingState!==U.HaveLocalOffer||s.signalingStateTransition(e);const i=s;if(this.delegate&&this.delegate.onAccept)this.delegate.onAccept({message:e,session:i,ack:e=>{const t=i.ack(e);return this.confirmedDialogAcks.set(i.id,t),t}});else{const e=i.ack();this.confirmedDialogAcks.set(i.id,e)}}return;case/^3[0-9]{2}$/.test(t):return this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),void(this.delegate&&this.delegate.onRedirect&&this.delegate.onRedirect({message:e}));case/^[4-6][0-9]{2}$/.test(t):return this.earlyDialogs.forEach((e=>e.dispose())),this.earlyDialogs.clear(),void(this.delegate&&this.delegate.onReject&&this.delegate.onReject({message:e}));default:throw new Error(`Invalid status code ${t}`)}throw new Error(`Executing what should be an unreachable code path receiving ${t} response.`)}}class Ze extends ke{constructor(e,t,s){super($e,e,t,s),this.core=e}dispose(){this.earlyDialog&&this.earlyDialog.dispose(),super.dispose()}accept(e={statusCode:200}){if(!this.acceptable)throw new B(`${this.message.method} not acceptable in state ${this.transaction.state}.`);if(!this.confirmedDialog)if(this.earlyDialog)this.earlyDialog.confirm(),this.confirmedDialog=this.earlyDialog,this.earlyDialog=void 0;else{const e=this.transaction;if(!(e instanceof $e))throw new Error("Transaction not instance of InviteClientTransaction.");const t=Ae.initialDialogStateForUserAgentServer(this.message,this.toTag);this.confirmedDialog=new Ke(e,this.core,t)}const t=this.message.getHeaders("record-route").map((e=>`Record-Route: ${e}`)),s=`Contact: ${this.core.configuration.contact.toString()}`,i="Allow: "+V.toString();if(!e.body)if(this.confirmedDialog.signalingState===U.Stable)e.body=this.confirmedDialog.answer;else if(this.confirmedDialog.signalingState===U.Initial||this.confirmedDialog.signalingState===U.HaveRemoteOffer)throw new Error("Response must have a body.");e.statusCode=e.statusCode||200,e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(t),e.extraHeaders.push(i),e.extraHeaders.push(s);const r=super.accept(e),n=this.confirmedDialog,o=Object.assign(Object.assign({},r),{session:n});return e.body&&this.confirmedDialog.signalingState!==U.Stable&&this.confirmedDialog.signalingStateTransition(e.body),o}progress(e={statusCode:180}){if(!this.progressable)throw new B(`${this.message.method} not progressable in state ${this.transaction.state}.`);if(!this.earlyDialog){const e=this.transaction;if(!(e instanceof $e))throw new Error("Transaction not instance of InviteClientTransaction.");const t=Ae.initialDialogStateForUserAgentServer(this.message,this.toTag,!0);this.earlyDialog=new Ke(e,this.core,t)}const t=this.message.getHeaders("record-route").map((e=>`Record-Route: ${e}`)),s=`Contact: ${this.core.configuration.contact}`;e.extraHeaders=e.extraHeaders||[],e.extraHeaders=e.extraHeaders.concat(t),e.extraHeaders.push(s);const i=super.progress(e),r=this.earlyDialog,n=Object.assign(Object.assign({},i),{session:r});return e.body&&this.earlyDialog.signalingState!==U.Stable&&this.earlyDialog.signalingStateTransition(e.body),n}redirect(e,t={statusCode:302}){return super.redirect(e,t)}reject(e={statusCode:486}){return super.reject(e)}}class Je extends He{constructor(e,t,s){super(Ce,e,t,s)}}class ze extends He{constructor(e,t,s){super(Ce,e,t,s)}}class Xe extends ke{constructor(e,t,s){super(xe,e,t,s),this.core=e}}class Qe extends He{constructor(e,t,s){const i=e.createOutgoingRequestMessage(G.SUBSCRIBE,s);super(Ce,e.userAgentCore,i,t),this.dialog=e}waitNotifyStop(){}receiveResponse(e){if(e.statusCode&&e.statusCode>=200&&e.statusCode<300){const t=e.getHeader("Expires");if(t){const e=Number(t);this.dialog.subscriptionExpires>e&&(this.dialog.subscriptionExpires=e)}else this.logger.warn("Expires header missing in a 200-class response to SUBSCRIBE")}if(e.statusCode&&e.statusCode>=400&&e.statusCode<700){[404,405,410,416,480,481,482,483,484,485,489,501,604].includes(e.statusCode)&&this.dialog.terminate()}super.receiveResponse(e)}}class et extends Ae{constructor(e,t,s,i,r,n){super(i,r),this.delegate=n,this._autoRefresh=!1,this._subscriptionEvent=e,this._subscriptionExpires=t,this._subscriptionExpiresInitial=t,this._subscriptionExpiresLastSet=Math.floor(Date.now()/1e3),this._subscriptionRefresh=void 0,this._subscriptionRefreshLastSet=void 0,this._subscriptionState=s,this.logger=i.loggerFactory.getLogger("sip.subscribe-dialog"),this.logger.log(`SUBSCRIBE dialog ${this.id} constructed`)}static initialDialogStateForSubscription(e,t){const s=t.getHeaders("record-route"),i=t.parseHeader("contact");if(!i)throw new Error("Contact undefined.");if(!(i instanceof v))throw new Error("Contact not instance of NameAddrHeader.");const r=i.uri,n=e.cseq,o=e.callId,a=e.fromTag,c=t.fromTag;if(!o)throw new Error("Call id undefined.");if(!a)throw new Error("From tag undefined.");if(!c)throw new Error("To tag undefined.");if(!e.from)throw new Error("From undefined.");if(!e.to)throw new Error("To undefined.");return{id:o+a+c,early:!1,callId:o,localTag:a,remoteTag:c,localSequenceNumber:n,remoteSequenceNumber:undefined,localURI:e.from.uri,remoteURI:e.to.uri,remoteTarget:r,routeSet:s,secure:!1}}dispose(){super.dispose(),this.N&&(clearTimeout(this.N),this.N=void 0),this.refreshTimerClear(),this.logger.log(`SUBSCRIBE dialog ${this.id} destroyed`)}get autoRefresh(){return this._autoRefresh}set autoRefresh(e){this._autoRefresh=!0,this.refreshTimerSet()}get subscriptionEvent(){return this._subscriptionEvent}get subscriptionExpires(){const e=Math.floor(Date.now()/1e3)-this._subscriptionExpiresLastSet,t=this._subscriptionExpires-e;return Math.max(t,0)}set subscriptionExpires(e){if(e<0)throw new Error("Expires must be greater than or equal to zero.");if(this._subscriptionExpires=e,this._subscriptionExpiresLastSet=Math.floor(Date.now()/1e3),this.autoRefresh){const t=this.subscriptionRefresh;(void 0===t||t>=e)&&this.refreshTimerSet()}}get subscriptionExpiresInitial(){return this._subscriptionExpiresInitial}get subscriptionRefresh(){if(void 0===this._subscriptionRefresh||void 0===this._subscriptionRefreshLastSet)return;const e=Math.floor(Date.now()/1e3)-this._subscriptionRefreshLastSet,t=this._subscriptionRefresh-e;return Math.max(t,0)}get subscriptionState(){return this._subscriptionState}receiveRequest(e){if(this.logger.log(`SUBSCRIBE dialog ${this.id} received ${e.method} request`),this.sequenceGuard(e))if(super.receiveRequest(e),e.method===G.NOTIFY)this.onNotify(e);else this.logger.log(`SUBSCRIBE dialog ${this.id} received unimplemented ${e.method} request`),this.core.replyStateless(e,{statusCode:501});else this.logger.log(`SUBSCRIBE dialog ${this.id} rejected out of order ${e.method} request.`)}refresh(){const e="Allow: "+V.toString(),t={};return t.extraHeaders=(t.extraHeaders||[]).slice(),t.extraHeaders.push(e),t.extraHeaders.push("Event: "+this.subscriptionEvent),t.extraHeaders.push("Expires: "+this.subscriptionExpiresInitial),t.extraHeaders.push("Contact: "+this.core.configuration.contact.toString()),this.subscribe(void 0,t)}subscribe(e,t={}){var s;if(this.subscriptionState!==re.Pending&&this.subscriptionState!==re.Active)throw new Error(`Invalid state ${this.subscriptionState}. May only re-subscribe while in state "pending" or "active".`);this.logger.log(`SUBSCRIBE dialog ${this.id} sending SUBSCRIBE request`);const i=new Qe(this,e,t);return this.N&&(clearTimeout(this.N),this.N=void 0),(null===(s=t.extraHeaders)||void 0===s?void 0:s.includes("Expires: 0"))||(this.N=setTimeout((()=>this.timerN()),L.TIMER_N)),i}terminate(){this.stateTransition(re.Terminated),this.onTerminated()}unsubscribe(){const e="Allow: "+V.toString(),t={};return t.extraHeaders=(t.extraHeaders||[]).slice(),t.extraHeaders.push(e),t.extraHeaders.push("Event: "+this.subscriptionEvent),t.extraHeaders.push("Expires: 0"),t.extraHeaders.push("Contact: "+this.core.configuration.contact.toString()),this.subscribe(void 0,t)}onNotify(e){const t=e.parseHeader("Event").event;if(!t||t!==this.subscriptionEvent)return void this.core.replyStateless(e,{statusCode:489});this.N&&(clearTimeout(this.N),this.N=void 0);const s=e.parseHeader("Subscription-State");if(!s||!s.state)return void this.core.replyStateless(e,{statusCode:489});const i=s.state,r=s.expires?Math.max(s.expires,0):void 0;switch(i){case"pending":this.stateTransition(re.Pending,r);break;case"active":this.stateTransition(re.Active,r);break;case"terminated":this.stateTransition(re.Terminated,r);break;default:this.logger.warn("Unrecognized subscription state.")}const n=new je(this,e);this.delegate&&this.delegate.onNotify?this.delegate.onNotify(n):n.accept()}onRefresh(e){this.delegate&&this.delegate.onRefresh&&this.delegate.onRefresh(e)}onTerminated(){this.delegate&&this.delegate.onTerminated&&this.delegate.onTerminated()}refreshTimerClear(){this.refreshTimer&&(clearTimeout(this.refreshTimer),this.refreshTimer=void 0)}refreshTimerSet(){if(this.refreshTimerClear(),this.autoRefresh&&this.subscriptionExpires>0){const e=900*this.subscriptionExpires;this._subscriptionRefresh=Math.floor(e/1e3),this._subscriptionRefreshLastSet=Math.floor(Date.now()/1e3),this.refreshTimer=setTimeout((()=>{this.refreshTimer=void 0,this._subscriptionRefresh=void 0,this._subscriptionRefreshLastSet=void 0,this.onRefresh(this.refresh())}),e)}}stateTransition(e,t){const s=()=>{this.logger.warn(`Invalid subscription state transition from ${this.subscriptionState} to ${e}`)};switch(e){case re.Initial:case re.NotifyWait:return void s();case re.Pending:if(this.subscriptionState!==re.NotifyWait&&this.subscriptionState!==re.Pending)return void s();break;case re.Active:case re.Terminated:if(this.subscriptionState!==re.NotifyWait&&this.subscriptionState!==re.Pending&&this.subscriptionState!==re.Active)return void s();break;default:return void s()}e===re.Pending&&t&&(this.subscriptionExpires=t),e===re.Active&&t&&(this.subscriptionExpires=t),e===re.Terminated&&this.dispose(),this._subscriptionState=e}timerN(){this.logger.warn("Timer N expired for SUBSCRIBE dialog. Timed out waiting for NOTIFY."),this.subscriptionState!==re.Terminated&&(this.stateTransition(re.Terminated),this.onTerminated())}}class tt extends He{constructor(e,t,s){const i=t.getHeader("Event");if(!i)throw new Error("Event undefined");const r=t.getHeader("Expires");if(!r)throw new Error("Expires undefined");super(Ce,e,t,s),this.delegate=s,this.subscriberId=t.callId+t.fromTag+i,this.subscriptionExpiresRequested=this.subscriptionExpires=Number(r),this.subscriptionEvent=i,this.subscriptionState=re.NotifyWait,this.waitNotifyStart()}dispose(){super.dispose()}onNotify(e){const t=e.message.parseHeader("Event").event;if(!t||t!==this.subscriptionEvent)return this.logger.warn("Failed to parse event."),void e.reject({statusCode:489});const s=e.message.parseHeader("Subscription-State");if(!s||!s.state)return this.logger.warn("Failed to parse subscription state."),void e.reject({statusCode:489});const i=s.state;switch(i){case"pending":case"active":case"terminated":break;default:return this.logger.warn(`Invalid subscription state ${i}`),void e.reject({statusCode:489})}if("terminated"!==i){if(!e.message.parseHeader("contact"))return this.logger.warn("Failed to parse contact."),void e.reject({statusCode:489})}if(this.dialog)throw new Error("Dialog already created. This implementation only supports install of single subscriptions.");switch(this.waitNotifyStop(),this.subscriptionExpires=s.expires?Math.min(this.subscriptionExpires,Math.max(s.expires,0)):this.subscriptionExpires,i){case"pending":this.subscriptionState=re.Pending;break;case"active":this.subscriptionState=re.Active;break;case"terminated":this.subscriptionState=re.Terminated;break;default:throw new Error(`Unrecognized state ${i}.`)}if(this.subscriptionState!==re.Terminated){const t=et.initialDialogStateForSubscription(this.message,e.message);this.dialog=new et(this.subscriptionEvent,this.subscriptionExpires,this.subscriptionState,this.core,t)}if(this.delegate&&this.delegate.onNotify){const t=e,s=this.dialog;this.delegate.onNotify({request:t,subscription:s})}else e.accept()}waitNotifyStart(){this.N||(this.core.subscribers.set(this.subscriberId,this),this.N=setTimeout((()=>this.timerN()),L.TIMER_N))}waitNotifyStop(){this.N&&(this.core.subscribers.delete(this.subscriberId),clearTimeout(this.N),this.N=void 0)}receiveResponse(e){if(this.authenticationGuard(e)){if(e.statusCode&&e.statusCode>=200&&e.statusCode<300){const t=e.getHeader("Expires");if(t){const e=Number(t);e>this.subscriptionExpiresRequested&&this.logger.warn("Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request"),ethis.subscriptionExpires&&(this.dialog.subscriptionExpires=this.subscriptionExpires)}e.statusCode&&e.statusCode>=300&&e.statusCode<700&&this.waitNotifyStop(),super.receiveResponse(e)}}timerN(){this.logger.warn("Timer N expired for SUBSCRIBE user agent client. Timed out waiting for NOTIFY."),this.waitNotifyStop(),this.delegate&&this.delegate.onNotifyTimeout&&this.delegate.onNotifyTimeout()}}class st extends ke{constructor(e,t,s){super(xe,e,t,s),this.core=e}}const it=["application/sdp","application/dtmf-relay"];class rt{constructor(e,t={}){this.userAgentClients=new Map,this.userAgentServers=new Map,this.configuration=e,this.delegate=t,this.dialogs=new Map,this.subscribers=new Map,this.logger=e.loggerFactory.getLogger("sip.user-agent-core")}dispose(){this.reset()}reset(){this.dialogs.forEach((e=>e.dispose())),this.dialogs.clear(),this.subscribers.forEach((e=>e.dispose())),this.subscribers.clear(),this.userAgentClients.forEach((e=>e.dispose())),this.userAgentClients.clear(),this.userAgentServers.forEach((e=>e.dispose())),this.userAgentServers.clear()}get loggerFactory(){return this.configuration.loggerFactory}get transport(){const e=this.configuration.transportAccessor();if(!e)throw new Error("Transport undefined.");return e}invite(e,t){return new Ye(this,e,t)}message(e,t){return new Ne(this,e,t)}publish(e,t){return new Je(this,e,t)}register(e,t){return new ze(this,e,t)}subscribe(e,t){return new tt(this,e,t)}request(e,t){return new He(Ce,this,e,t)}makeOutgoingRequestMessage(e,t,s,i,r,n,o){const a=this.configuration.sipjsId,c=this.configuration.displayName,h=this.configuration.viaForceRport,d=this.configuration.hackViaTcp,l=this.configuration.supportedOptionTags.slice();e===G.REGISTER&&l.push("path","gruu"),e===G.INVITE&&(this.configuration.contact.pubGruu||this.configuration.contact.tempGruu)&&l.push("gruu");const g={callIdPrefix:a,forceRport:h,fromDisplayName:c,hackViaTcp:d,optionTags:l,routeSet:this.configuration.routeSet,userAgentString:this.configuration.userAgentHeaderFieldValue,viaHost:this.configuration.viaHost},u=Object.assign(Object.assign({},g),r);return new k(e,t,s,i,u,n,o)}receiveIncomingRequestFromTransport(e){this.receiveRequestFromTransport(e)}receiveIncomingResponseFromTransport(e){this.receiveResponseFromTransport(e)}replyStateless(e,t){const s=this.configuration.userAgentHeaderFieldValue,i=this.configuration.supportedOptionTagsResponse;t=Object.assign(Object.assign({},t),{userAgent:s,supported:i});const r=Se(e,t);return this.transport.send(r.message).catch((t=>{t instanceof Error&&this.logger.error(t.message),this.logger.error(`Transport error occurred sending stateless reply to ${e.method} request.`)})),r}receiveRequestFromTransport(e){const t=e.viaBranch,s=this.userAgentServers.get(t);e.method===G.ACK&&s&&s.transaction.state===de.Accepted&&s instanceof Ze?this.logger.warn(`Discarding out of dialog ACK after 2xx response sent on transaction ${t}.`):e.method!==G.CANCEL?s?s.transaction.receiveRequest(e):this.receiveRequest(e):s?(this.replyStateless(e,{statusCode:200}),s.transaction instanceof $e&&s.transaction.state===de.Proceeding&&s instanceof Ze&&s.receiveCancel(e)):this.replyStateless(e,{statusCode:481})}receiveRequest(e){if(!V.includes(e.method)){const t="Allow: "+V.toString();return void this.replyStateless(e,{statusCode:405,extraHeaders:[t]})}if(!e.ruri)throw new Error("Request-URI undefined.");if("sip"!==e.ruri.scheme)return void this.replyStateless(e,{statusCode:416});const t=e.ruri,s=e=>!!e&&e.user===t.user;if(!s(this.configuration.aor)&&!(s(this.configuration.contact.uri)||s(this.configuration.contact.pubGruu)||s(this.configuration.contact.tempGruu)))return this.logger.warn("Request-URI does not point to us."),void(e.method!==G.ACK&&this.replyStateless(e,{statusCode:404}));if(e.method!==G.INVITE||e.hasHeader("Contact")){if(!e.toTag){const t=e.viaBranch;if(!this.userAgentServers.has(t)){if(Array.from(this.userAgentServers.values()).some((t=>t.transaction.request.fromTag===e.fromTag&&t.transaction.request.callId===e.callId&&t.transaction.request.cseq===e.cseq)))return void this.replyStateless(e,{statusCode:482})}}e.toTag?this.receiveInsideDialogRequest(e):this.receiveOutsideDialogRequest(e)}else this.replyStateless(e,{statusCode:400,reasonPhrase:"Missing Contact Header"})}receiveInsideDialogRequest(e){if(e.method===G.NOTIFY){const t=e.parseHeader("Event");if(!t||!t.event)return void this.replyStateless(e,{statusCode:489});const s=e.callId+e.toTag+t.event,i=this.subscribers.get(s);if(i){const t=new je(this,e);return void i.onNotify(t)}}const t=e.callId+e.toTag+e.fromTag,s=this.dialogs.get(t);if(s){if(e.method===G.OPTIONS){const t="Allow: "+V.toString(),s="Accept: "+it.toString();return void this.replyStateless(e,{statusCode:200,extraHeaders:[t,s]})}s.receiveRequest(e)}else e.method!==G.ACK&&this.replyStateless(e,{statusCode:481})}receiveOutsideDialogRequest(e){switch(e.method){case G.ACK:break;case G.BYE:this.replyStateless(e,{statusCode:481});break;case G.CANCEL:throw new Error(`Unexpected out of dialog request method ${e.method}.`);case G.INFO:this.replyStateless(e,{statusCode:405});break;case G.INVITE:{const t=new Ze(this,e);this.delegate.onInvite?this.delegate.onInvite(t):t.reject()}break;case G.MESSAGE:{const t=new Oe(this,e);this.delegate.onMessage?this.delegate.onMessage(t):t.accept()}break;case G.NOTIFY:{const t=new je(this,e);this.delegate.onNotify?this.delegate.onNotify(t):t.reject({statusCode:405})}break;case G.OPTIONS:{const t="Allow: "+V.toString(),s="Accept: "+it.toString();this.replyStateless(e,{statusCode:200,extraHeaders:[t,s]})}break;case G.REFER:{const t=new We(this,e);this.delegate.onRefer?this.delegate.onRefer(t):t.reject({statusCode:405})}break;case G.REGISTER:{const t=new Xe(this,e);this.delegate.onRegister?this.delegate.onRegister(t):t.reject({statusCode:405})}break;case G.SUBSCRIBE:{const t=new st(this,e);this.delegate.onSubscribe?this.delegate.onSubscribe(t):t.reject({statusCode:480})}break;default:throw new Error(`Unexpected out of dialog request method ${e.method}.`)}}receiveResponseFromTransport(e){if(e.getHeaders("via").length>1)return void this.logger.warn("More than one Via header field present in the response, dropping");const t=e.viaBranch+e.method,s=this.userAgentClients.get(t);s?s.transaction.receiveResponse(e):this.logger.warn(`Discarding unmatched ${e.statusCode} response to ${e.method} ${t}.`)}}function nt(){return e=>e.audio||e.video?void 0===navigator.mediaDevices?Promise.reject(new Error("Media devices not available in insecure contexts.")):navigator.mediaDevices.getUserMedia.call(navigator.mediaDevices,e):Promise.resolve(new MediaStream)}function ot(){return{bundlePolicy:"balanced",certificates:void 0,iceCandidatePoolSize:0,iceServers:[{urls:"stun:stun.l.google.com:19302"}],iceTransportPolicy:"all",rtcpMuxPolicy:"require"}}class at{constructor(e,t,s){e.debug("SessionDescriptionHandler.constructor"),this.logger=e,this.mediaStreamFactory=t,this.sessionDescriptionHandlerConfiguration=s,this._localMediaStream=new MediaStream,this._remoteMediaStream=new MediaStream,this._peerConnection=new RTCPeerConnection(null==s?void 0:s.peerConnectionConfiguration),this.initPeerConnectionEventHandlers()}get localMediaStream(){return this._localMediaStream}get remoteMediaStream(){return this._remoteMediaStream}get dataChannel(){return this._dataChannel}get peerConnection(){return this._peerConnection}get peerConnectionDelegate(){return this._peerConnectionDelegate}set peerConnectionDelegate(e){this._peerConnectionDelegate=e}static dispatchAddTrackEvent(e,t){e.dispatchEvent(new MediaStreamTrackEvent("addtrack",{track:t}))}static dispatchRemoveTrackEvent(e,t){e.dispatchEvent(new MediaStreamTrackEvent("removetrack",{track:t}))}close(){this.logger.debug("SessionDescriptionHandler.close"),void 0!==this._peerConnection&&(this._peerConnection.getReceivers().forEach((e=>{e.track&&e.track.stop()})),this._peerConnection.getSenders().forEach((e=>{e.track&&e.track.stop()})),this._dataChannel&&this._dataChannel.close(),this._peerConnection.close(),this._peerConnection=void 0)}enableReceiverTracks(e){const t=this.peerConnection;if(!t)throw new Error("Peer connection closed.");t.getReceivers().forEach((t=>{t.track&&(t.track.enabled=e)}))}enableSenderTracks(e){const t=this.peerConnection;if(!t)throw new Error("Peer connection closed.");t.getSenders().forEach((t=>{t.track&&(t.track.enabled=e)}))}getDescription(e,t){var s,i;if(this.logger.debug("SessionDescriptionHandler.getDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));this.onDataChannel=null==e?void 0:e.onDataChannel;const r=null===(s=null==e?void 0:e.offerOptions)||void 0===s?void 0:s.iceRestart,n=void 0===(null==e?void 0:e.iceGatheringTimeout)?null===(i=this.sessionDescriptionHandlerConfiguration)||void 0===i?void 0:i.iceGatheringTimeout:null==e?void 0:e.iceGatheringTimeout;return this.getLocalMediaStream(e).then((()=>this.updateDirection(e))).then((()=>this.createDataChannel(e))).then((()=>this.createLocalOfferOrAnswer(e))).then((e=>this.applyModifiers(e,t))).then((e=>this.setLocalSessionDescription(e))).then((()=>this.waitForIceGatheringComplete(r,n))).then((()=>this.getLocalSessionDescription())).then((e=>({body:e.sdp,contentType:"application/sdp"}))).catch((e=>{throw this.logger.error("SessionDescriptionHandler.getDescription failed - "+e),e}))}hasDescription(e){return this.logger.debug("SessionDescriptionHandler.hasDescription"),"application/sdp"===e}iceGatheringComplete(){this.logger.debug("SessionDescriptionHandler.iceGatheringComplete"),void 0!==this.iceGatheringCompleteTimeoutId&&(this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - clearing timeout"),clearTimeout(this.iceGatheringCompleteTimeoutId),this.iceGatheringCompleteTimeoutId=void 0),void 0!==this.iceGatheringCompletePromise&&(this.logger.debug("SessionDescriptionHandler.iceGatheringComplete - resolving promise"),this.iceGatheringCompleteResolve&&this.iceGatheringCompleteResolve(),this.iceGatheringCompletePromise=void 0,this.iceGatheringCompleteResolve=void 0,this.iceGatheringCompleteReject=void 0)}sendDtmf(e,t){if(this.logger.debug("SessionDescriptionHandler.sendDtmf"),void 0===this._peerConnection)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - peer connection closed"),!1;const s=this._peerConnection.getSenders();if(0===s.length)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - no senders"),!1;const i=s[0].dtmf;if(!i)return this.logger.error("SessionDescriptionHandler.sendDtmf failed - no DTMF sender"),!1;const r=null==t?void 0:t.duration,n=null==t?void 0:t.interToneGap;try{i.insertDTMF(e,r,n)}catch(e){return this.logger.error(e.toString()),!1}return this.logger.log("SessionDescriptionHandler.sendDtmf sent via RTP: "+e.toString()),!0}setDescription(e,t,s){if(this.logger.debug("SessionDescriptionHandler.setDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));this.onDataChannel=null==t?void 0:t.onDataChannel;const i="have-local-offer"===this._peerConnection.signalingState?"answer":"offer";return this.getLocalMediaStream(t).then((()=>this.applyModifiers({sdp:e,type:i},s))).then((e=>this.setRemoteSessionDescription(e))).catch((e=>{throw this.logger.error("SessionDescriptionHandler.setDescription failed - "+e),e}))}applyModifiers(e,t){return this.logger.debug("SessionDescriptionHandler.applyModifiers"),t&&0!==t.length?t.reduce(((e,t)=>e.then(t)),Promise.resolve(e)).then((e=>{if(this.logger.debug("SessionDescriptionHandler.applyModifiers - modified sdp"),!e.sdp||!e.type)throw new Error("Invalid SDP.");return{sdp:e.sdp,type:e.type}})):Promise.resolve(e)}createDataChannel(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));if(!0!==(null==e?void 0:e.dataChannel))return Promise.resolve();if(this._dataChannel)return Promise.resolve();switch(this._peerConnection.signalingState){case"stable":this.logger.debug("SessionDescriptionHandler.createDataChannel - creating data channel");try{return this._dataChannel=this._peerConnection.createDataChannel((null==e?void 0:e.dataChannelLabel)||"",null==e?void 0:e.dataChannelOptions),this.onDataChannel&&this.onDataChannel(this._dataChannel),Promise.resolve()}catch(e){return Promise.reject(e)}case"have-remote-offer":return Promise.resolve();default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}}createLocalOfferOrAnswer(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));switch(this._peerConnection.signalingState){case"stable":return this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP offer"),this._peerConnection.createOffer(null==e?void 0:e.offerOptions);case"have-remote-offer":return this.logger.debug("SessionDescriptionHandler.createLocalOfferOrAnswer - creating SDP answer"),this._peerConnection.createAnswer(null==e?void 0:e.answerOptions);default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}}getLocalMediaStream(e){if(this.logger.debug("SessionDescriptionHandler.getLocalMediaStream"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));let t=Object.assign({},null==e?void 0:e.constraints);if(this.localMediaStreamConstraints){if(t.audio=t.audio||this.localMediaStreamConstraints.audio,t.video=t.video||this.localMediaStreamConstraints.video,JSON.stringify(this.localMediaStreamConstraints.audio)===JSON.stringify(t.audio)&&JSON.stringify(this.localMediaStreamConstraints.video)===JSON.stringify(t.video))return Promise.resolve()}else void 0===t.audio&&void 0===t.video&&(t={audio:!0});return this.localMediaStreamConstraints=t,this.mediaStreamFactory(t,this,e).then((e=>this.setLocalMediaStream(e)))}setLocalMediaStream(e){if(this.logger.debug("SessionDescriptionHandler.setLocalMediaStream"),!this._peerConnection)throw new Error("Peer connection undefined.");const t=this._peerConnection,s=this._localMediaStream,i=[],r=e=>{const r=e.kind;if("audio"!==r&&"video"!==r)throw new Error(`Unknown new track kind ${r}.`);const n=t.getSenders().find((e=>e.track&&e.track.kind===r));n?i.push(new Promise((e=>{this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - replacing sender ${r} track`),e()})).then((()=>n.replaceTrack(e).then((()=>{const t=s.getTracks().find((e=>e.kind===r));t&&(t.stop(),s.removeTrack(t),at.dispatchRemoveTrackEvent(s,t)),s.addTrack(e),at.dispatchAddTrackEvent(s,e)})).catch((e=>{throw this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to replace sender ${r} track`),e}))))):i.push(new Promise((e=>{this.logger.debug(`SessionDescriptionHandler.setLocalMediaStream - adding sender ${r} track`),e()})).then((()=>{try{t.addTrack(e,s)}catch(e){throw this.logger.error(`SessionDescriptionHandler.setLocalMediaStream - failed to add sender ${r} track`),e}s.addTrack(e),at.dispatchAddTrackEvent(s,e)})))},n=e.getAudioTracks();n.length&&r(n[0]);const o=e.getVideoTracks();return o.length&&r(o[0]),i.reduce(((e,t)=>e.then((()=>t))),Promise.resolve())}getLocalSessionDescription(){if(this.logger.debug("SessionDescriptionHandler.getLocalSessionDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));const e=this._peerConnection.localDescription;return e?Promise.resolve(e):Promise.reject(new Error("Failed to get local session description"))}setLocalSessionDescription(e){return this.logger.debug("SessionDescriptionHandler.setLocalSessionDescription"),void 0===this._peerConnection?Promise.reject(new Error("Peer connection closed.")):this._peerConnection.setLocalDescription(e)}setRemoteSessionDescription(e){if(this.logger.debug("SessionDescriptionHandler.setRemoteSessionDescription"),void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));const t=e.sdp;let s;switch(this._peerConnection.signalingState){case"stable":s="offer";break;case"have-local-offer":s="answer";break;default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}return t?this._peerConnection.setRemoteDescription({sdp:t,type:s}):(this.logger.error("SessionDescriptionHandler.setRemoteSessionDescription failed - cannot set null sdp"),Promise.reject(new Error("SDP is undefined")))}setRemoteTrack(e){this.logger.debug("SessionDescriptionHandler.setRemoteTrack");const t=this._remoteMediaStream;t.getTrackById(e.id)?this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - have remote ${e.kind} track`):"audio"===e.kind?(this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${e.kind} track`),t.getAudioTracks().forEach((e=>{e.stop(),t.removeTrack(e),at.dispatchRemoveTrackEvent(t,e)})),t.addTrack(e),at.dispatchAddTrackEvent(t,e)):"video"===e.kind&&(this.logger.debug(`SessionDescriptionHandler.setRemoteTrack - adding remote ${e.kind} track`),t.getVideoTracks().forEach((e=>{e.stop(),t.removeTrack(e),at.dispatchRemoveTrackEvent(t,e)})),t.addTrack(e),at.dispatchAddTrackEvent(t,e))}updateDirection(e){if(void 0===this._peerConnection)return Promise.reject(new Error("Peer connection closed."));switch(this._peerConnection.signalingState){case"stable":this.logger.debug("SessionDescriptionHandler.updateDirection - setting offer direction");{const t=t=>{switch(t){case"inactive":case"recvonly":return(null==e?void 0:e.hold)?"inactive":"recvonly";case"sendonly":case"sendrecv":return(null==e?void 0:e.hold)?"sendonly":"sendrecv";case"stopped":return"stopped";default:throw new Error("Should never happen")}};this._peerConnection.getTransceivers().forEach((e=>{if(e.direction){const s=t(e.direction);e.direction!==s&&(e.direction=s)}}))}break;case"have-remote-offer":this.logger.debug("SessionDescriptionHandler.updateDirection - setting answer direction");{const t=(()=>{const e=this._peerConnection.remoteDescription;if(!e)throw new Error("Failed to read remote offer");const t=/a=sendrecv\r\n|a=sendonly\r\n|a=recvonly\r\n|a=inactive\r\n/.exec(e.sdp);if(t)switch(t[0]){case"a=inactive\r\n":return"inactive";case"a=recvonly\r\n":return"recvonly";case"a=sendonly\r\n":return"sendonly";case"a=sendrecv\r\n":return"sendrecv";default:throw new Error("Should never happen")}return"sendrecv"})(),s=(()=>{switch(t){case"inactive":return"inactive";case"recvonly":return"sendonly";case"sendonly":return(null==e?void 0:e.hold)?"inactive":"recvonly";case"sendrecv":return(null==e?void 0:e.hold)?"sendonly":"sendrecv";default:throw new Error("Should never happen")}})();this._peerConnection.getTransceivers().forEach((e=>{e.direction&&"stopped"!==e.direction&&e.direction!==s&&(e.direction=s)}))}break;default:return Promise.reject(new Error("Invalid signaling state "+this._peerConnection.signalingState))}return Promise.resolve()}waitForIceGatheringComplete(e=!1,t=0){return this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete"),void 0===this._peerConnection?Promise.reject("Peer connection closed."):e||"complete"!==this._peerConnection.iceGatheringState?(void 0!==this.iceGatheringCompletePromise&&(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - rejecting prior waiting promise"),this.iceGatheringCompleteReject&&this.iceGatheringCompleteReject(new Error("Promise superseded.")),this.iceGatheringCompletePromise=void 0,this.iceGatheringCompleteResolve=void 0,this.iceGatheringCompleteReject=void 0),this.iceGatheringCompletePromise=new Promise(((e,s)=>{this.iceGatheringCompleteResolve=e,this.iceGatheringCompleteReject=s,t>0&&(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout in "+t),this.iceGatheringCompleteTimeoutId=setTimeout((()=>{this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - timeout"),this.iceGatheringComplete()}),t))})),this.iceGatheringCompletePromise):(this.logger.debug("SessionDescriptionHandler.waitForIceGatheringToComplete - already complete"),Promise.resolve())}initPeerConnectionEventHandlers(){if(this.logger.debug("SessionDescriptionHandler.initPeerConnectionEventHandlers"),!this._peerConnection)throw new Error("Peer connection undefined.");const e=this._peerConnection;e.onconnectionstatechange=t=>{var s;const i=e.connectionState;this.logger.debug(`SessionDescriptionHandler.onconnectionstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onconnectionstatechange)&&this._peerConnectionDelegate.onconnectionstatechange(t)},e.ondatachannel=e=>{var t;this.logger.debug("SessionDescriptionHandler.ondatachannel"),this._dataChannel=e.channel,this.onDataChannel&&this.onDataChannel(this._dataChannel),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.ondatachannel)&&this._peerConnectionDelegate.ondatachannel(e)},e.onicecandidate=e=>{var t;this.logger.debug("SessionDescriptionHandler.onicecandidate"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onicecandidate)&&this._peerConnectionDelegate.onicecandidate(e)},e.onicecandidateerror=e=>{var t;this.logger.debug("SessionDescriptionHandler.onicecandidateerror"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onicecandidateerror)&&this._peerConnectionDelegate.onicecandidateerror(e)},e.oniceconnectionstatechange=t=>{var s;const i=e.iceConnectionState;this.logger.debug(`SessionDescriptionHandler.oniceconnectionstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.oniceconnectionstatechange)&&this._peerConnectionDelegate.oniceconnectionstatechange(t)},e.onicegatheringstatechange=t=>{var s;const i=e.iceGatheringState;this.logger.debug(`SessionDescriptionHandler.onicegatheringstatechange ${i}`),"complete"===i&&this.iceGatheringComplete(),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onicegatheringstatechange)&&this._peerConnectionDelegate.onicegatheringstatechange(t)},e.onnegotiationneeded=e=>{var t;this.logger.debug("SessionDescriptionHandler.onnegotiationneeded"),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.onnegotiationneeded)&&this._peerConnectionDelegate.onnegotiationneeded(e)},e.onsignalingstatechange=t=>{var s;const i=e.signalingState;this.logger.debug(`SessionDescriptionHandler.onsignalingstatechange ${i}`),(null===(s=this._peerConnectionDelegate)||void 0===s?void 0:s.onsignalingstatechange)&&this._peerConnectionDelegate.onsignalingstatechange(t)},e.ontrack=e=>{var t;const s=e.track.kind,i=e.track.enabled?"enabled":"disabled";this.logger.debug(`SessionDescriptionHandler.ontrack ${s} ${i}`),this.setRemoteTrack(e.track),(null===(t=this._peerConnectionDelegate)||void 0===t?void 0:t.ontrack)&&this._peerConnectionDelegate.ontrack(e)}}}function ct(e){return(t,s)=>{void 0===e&&(e=nt());const i={iceGatheringTimeout:void 0!==(null==s?void 0:s.iceGatheringTimeout)?null==s?void 0:s.iceGatheringTimeout:5e3,peerConnectionConfiguration:Object.assign(Object.assign({},{bundlePolicy:"balanced",certificates:void 0,iceCandidatePoolSize:0,iceServers:[{urls:"stun:stun.l.google.com:19302"}],iceTransportPolicy:"all",rtcpMuxPolicy:"require"}),null==s?void 0:s.peerConnectionConfiguration)},r=t.userAgent.getLogger("sip.SessionDescriptionHandler");return new at(r,e,i)}}class ht{constructor(e,t){if(this._state=oe.Disconnected,this.transitioningState=!1,this._stateEventEmitter=new p,this.logger=e,t){const e=t,s=null==e?void 0:e.wsServers,i=null==e?void 0:e.maxReconnectionAttempts;if(void 0!==s){const e='The transport option "wsServers" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==i){const e='The transport option "maxReconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}s&&!t.server&&("string"==typeof s&&(t.server=s),s instanceof Array&&(t.server=s[0]))}this.configuration=Object.assign(Object.assign({},ht.defaultOptions),t);const s=this.configuration.server,i=y.parse(s,"absoluteURI");if(-1===i)throw this.logger.error(`Invalid WebSocket Server URL "${s}"`),new Error("Invalid WebSocket Server URL");if(!["wss","ws","udp"].includes(i.scheme))throw this.logger.error(`Invalid scheme in WebSocket Server URL "${s}"`),new Error("Invalid scheme in WebSocket Server URL");this._protocol=i.scheme.toUpperCase()}dispose(){return this.disconnect()}get protocol(){return this._protocol}get server(){return this.configuration.server}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get ws(){return this._ws}connect(){return this._connect()}disconnect(){return this._disconnect()}isConnected(){return this.state===oe.Connected}send(e){return this._send(e)}_connect(){switch(this.logger.log(`Connecting ${this.server}`),this.state){case oe.Connecting:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Connecting));if(!this.connectPromise)throw new Error("Connect promise must be defined.");return this.connectPromise;case oe.Connected:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Connecting));if(this.connectPromise)throw new Error("Connect promise must not be defined.");return Promise.resolve();case oe.Disconnecting:if(this.connectPromise)throw new Error("Connect promise must not be defined.");try{this.transitionState(oe.Connecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Disconnected:if(this.connectPromise)throw new Error("Connect promise must not be defined.");try{this.transitionState(oe.Connecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;default:throw new Error("Unknown state")}let e;try{e=new WebSocket(this.server,"sip"),e.binaryType="arraybuffer",e.addEventListener("close",(t=>this.onWebSocketClose(t,e))),e.addEventListener("error",(t=>this.onWebSocketError(t,e))),e.addEventListener("open",(t=>this.onWebSocketOpen(t,e))),e.addEventListener("message",(t=>this.onWebSocketMessage(t,e))),this._ws=e}catch(e){return this._ws=void 0,this.logger.error("WebSocket construction failed."),this.logger.error(e.toString()),new Promise(((t,s)=>{this.connectResolve=t,this.connectReject=s,this.transitionState(oe.Disconnected,e)}))}return this.connectPromise=new Promise(((t,s)=>{this.connectResolve=t,this.connectReject=s,this.connectTimeout=setTimeout((()=>{this.logger.warn("Connect timed out. Exceeded time set in configuration.connectionTimeout: "+this.configuration.connectionTimeout+"s."),e.close(1e3)}),1e3*this.configuration.connectionTimeout)})),this.connectPromise}_disconnect(){switch(this.logger.log(`Disconnecting ${this.server}`),this.state){case oe.Connecting:if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");try{this.transitionState(oe.Disconnecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Connected:if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");try{this.transitionState(oe.Disconnecting)}catch(e){if(e instanceof d)return Promise.reject(e);throw e}break;case oe.Disconnecting:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Disconnecting));if(!this.disconnectPromise)throw new Error("Disconnect promise must be defined.");return this.disconnectPromise;case oe.Disconnected:if(this.transitioningState)return Promise.reject(this.transitionLoopDetectedError(oe.Disconnecting));if(this.disconnectPromise)throw new Error("Disconnect promise must not be defined.");return Promise.resolve();default:throw new Error("Unknown state")}if(!this._ws)throw new Error("WebSocket must be defined.");const e=this._ws;return this.disconnectPromise=new Promise(((t,s)=>{this.disconnectResolve=t,this.disconnectReject=s;try{e.close(1e3)}catch(e){throw this.logger.error("WebSocket close failed."),this.logger.error(e.toString()),e}})),this.disconnectPromise}_send(e){if(!0===this.configuration.traceSip&&this.logger.log("Sending WebSocket message:\n\n"+e+"\n"),this._state!==oe.Connected)return Promise.reject(new Error("Not connected."));if(!this._ws)throw new Error("WebSocket undefined.");try{this._ws.send(e)}catch(e){return e instanceof Error?Promise.reject(e):Promise.reject(new Error("WebSocket send failed."))}return Promise.resolve()}onWebSocketClose(e,t){if(t!==this._ws)return;const s=`WebSocket closed ${this.server} (code: ${e.code})`,i=this.disconnectPromise?void 0:new Error(s);i&&this.logger.warn("WebSocket closed unexpectedly"),this.logger.log(s),this._ws=void 0,this.transitionState(oe.Disconnected,i)}onWebSocketError(e,t){t===this._ws&&this.logger.error("WebSocket error occurred.")}onWebSocketMessage(e,t){if(t!==this._ws)return;const s=e.data;let i;if(/^(\r\n)+$/.test(s))return this.clearKeepAliveTimeout(),void(!0===this.configuration.traceSip&&this.logger.log("Received WebSocket message with CRLF Keep Alive response"));if(s){if("string"!=typeof s){try{i=(new TextDecoder).decode(new Uint8Array(s))}catch(e){return this.logger.error(e.toString()),void this.logger.error("Received WebSocket binary message failed to be converted into string, message discarded")}!0===this.configuration.traceSip&&this.logger.log("Received WebSocket binary message:\n\n"+i+"\n")}else i=s,!0===this.configuration.traceSip&&this.logger.log("Received WebSocket text message:\n\n"+i+"\n");if(this.state===oe.Connected){if(this.onMessage)try{this.onMessage(i)}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onMessage callback"),e}}else this.logger.warn("Received message while not connected, discarding...")}else this.logger.warn("Received empty message, discarding...")}onWebSocketOpen(e,t){t===this._ws&&this._state===oe.Connecting&&(this.logger.log(`WebSocket opened ${this.server}`),this.transitionState(oe.Connected))}transitionLoopDetectedError(e){let t="A state transition loop has been detected.";return t+=` An attempt to transition from ${this._state} to ${e} before the prior transition completed.`,t+=" Perhaps you are synchronously calling connect() or disconnect() from a callback or state change handler?",this.logger.error(t),new d("Loop detected.")}transitionState(e,t){const s=()=>{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};if(this.transitioningState)throw this.transitionLoopDetectedError(e);switch(this.transitioningState=!0,this._state){case oe.Connecting:e!==oe.Connected&&e!==oe.Disconnecting&&e!==oe.Disconnected&&s();break;case oe.Connected:e!==oe.Disconnecting&&e!==oe.Disconnected&&s();break;case oe.Disconnecting:e!==oe.Connecting&&e!==oe.Disconnected&&s();break;case oe.Disconnected:e!==oe.Connecting&&s();break;default:throw new Error("Unknown state.")}const i=this._state;this._state=e;const r=this.connectResolve,n=this.connectReject;i===oe.Connecting&&(this.connectPromise=void 0,this.connectResolve=void 0,this.connectReject=void 0);const o=this.disconnectResolve,a=this.disconnectReject;if(i===oe.Disconnecting&&(this.disconnectPromise=void 0,this.disconnectResolve=void 0,this.disconnectReject=void 0),this.connectTimeout&&(clearTimeout(this.connectTimeout),this.connectTimeout=void 0),this.logger.log(`Transitioned from ${i} to ${this._state}`),this._stateEventEmitter.emit(this._state),e===oe.Connected&&(this.startSendingKeepAlives(),this.onConnect))try{this.onConnect()}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onConnect callback"),e}if(i===oe.Connected&&(this.stopSendingKeepAlives(),this.onDisconnect))try{t?this.onDisconnect(t):this.onDisconnect()}catch(e){throw this.logger.error(e.toString()),this.logger.error("Exception thrown by onDisconnect callback"),e}if(i===oe.Connecting){if(!r)throw new Error("Connect resolve undefined.");if(!n)throw new Error("Connect reject undefined.");e===oe.Connected?r():n(t||new Error("Connect aborted."))}if(i===oe.Disconnecting){if(!o)throw new Error("Disconnect resolve undefined.");if(!a)throw new Error("Disconnect reject undefined.");e===oe.Disconnected?o():a(t||new Error("Disconnect aborted."))}this.transitioningState=!1}clearKeepAliveTimeout(){this.keepAliveDebounceTimeout&&clearTimeout(this.keepAliveDebounceTimeout),this.keepAliveDebounceTimeout=void 0}sendKeepAlive(){return this.keepAliveDebounceTimeout?Promise.resolve():(this.keepAliveDebounceTimeout=setTimeout((()=>{this.clearKeepAliveTimeout()}),1e3*this.configuration.keepAliveDebounce),this.send("\r\n\r\n"))}startSendingKeepAlives(){this.configuration.keepAliveInterval&&!this.keepAliveInterval&&(this.keepAliveInterval=setInterval((()=>{this.sendKeepAlive(),this.startSendingKeepAlives()}),(e=>{const t=.8*e;return 1e3*(Math.random()*(e-t)+t)})(this.configuration.keepAliveInterval)))}stopSendingKeepAlives(){this.keepAliveInterval&&clearInterval(this.keepAliveInterval),this.keepAliveDebounceTimeout&&clearTimeout(this.keepAliveDebounceTimeout),this.keepAliveInterval=void 0,this.keepAliveDebounceTimeout=void 0}}ht.defaultOptions={server:"",connectionTimeout:5,keepAliveInterval:0,keepAliveDebounce:10,traceSip:!0};class dt{constructor(e={}){if(this._publishers={},this._registerers={},this._sessions={},this._subscriptions={},this._state=ae.Stopped,this._stateEventEmitter=new p,this.delegate=e.delegate,this.options=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},dt.defaultOptions()),{sipjsId:$(5)}),{uri:new w("sip","anonymous."+$(6),"anonymous.invalid")}),{viaHost:$(12)+".invalid"}),dt.stripUndefinedProperties(e)),this.options.hackIpInContact)if("boolean"==typeof this.options.hackIpInContact&&this.options.hackIpInContact){const e=1,t=254,s=Math.floor(Math.random()*(t-e+1)+e);this.options.viaHost="192.0.2."+s}else this.options.hackIpInContact&&(this.options.viaHost=this.options.hackIpInContact);switch(this.loggerFactory=new Te,this.logger=this.loggerFactory.getLogger("sip.UserAgent"),this.loggerFactory.builtinEnabled=this.options.logBuiltinEnabled,this.loggerFactory.connector=this.options.logConnector,this.options.logLevel){case"error":this.loggerFactory.level=ce.error;break;case"warn":this.loggerFactory.level=ce.warn;break;case"log":this.loggerFactory.level=ce.log;break;case"debug":this.loggerFactory.level=ce.debug}if(this.options.logConfiguration&&(this.logger.log("Configuration:"),Object.keys(this.options).forEach((e=>{const t=this.options[e];switch(e){case"uri":case"sessionDescriptionHandlerFactory":this.logger.log("\xb7 "+e+": "+t);break;case"authorizationPassword":this.logger.log("\xb7 "+e+": NOT SHOWN");break;case"transportConstructor":this.logger.log("\xb7 "+e+": "+t.name);break;default:this.logger.log("\xb7 "+e+": "+JSON.stringify(t))}}))),this.options.transportOptions){const t=this.options.transportOptions,s=t.maxReconnectionAttempts,i=t.reconnectionTimeout;if(void 0!==s){const e='The transport option "maxReconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==i){const e='The transport option "reconnectionTimeout" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}void 0===e.reconnectionDelay&&void 0!==i&&(this.options.reconnectionDelay=i),void 0===e.reconnectionAttempts&&void 0!==s&&(this.options.reconnectionAttempts=s)}if(void 0!==e.reconnectionDelay){const e='The user agent option "reconnectionDelay" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(void 0!==e.reconnectionAttempts){const e='The user agent option "reconnectionAttempts" as has apparently been specified and has been deprecated. It will no longer be available starting with SIP.js release 0.16.0. Please update accordingly.';this.logger.warn(e)}if(this._transport=new this.options.transportConstructor(this.getLogger("sip.Transport"),this.options.transportOptions),this.initTransportCallbacks(),this._contact=this.initContact(),this._instanceId=this.options.instanceId?this.options.instanceId:dt.newUUID(),-1===y.parse(this._instanceId,"uuid"))throw new Error("Invalid instanceId.");this._userAgentCore=this.initCore()}static makeURI(e){return y.URIParse(e)}static defaultOptions(){return{allowLegacyNotifications:!1,authorizationHa1:"",authorizationPassword:"",authorizationUsername:"",delegate:{},contactName:"",contactParams:{transport:"ws"},displayName:"",forceRport:!1,gracefulShutdown:!0,hackAllowUnregisteredOptionTags:!1,hackIpInContact:!1,hackViaTcp:!1,instanceId:"",instanceIdAlwaysAdded:!1,logBuiltinEnabled:!0,logConfiguration:!0,logConnector:()=>{},logLevel:"log",noAnswerTimeout:60,preloadedRouteSet:[],reconnectionAttempts:0,reconnectionDelay:4,sendInitialProvisionalResponse:!0,sessionDescriptionHandlerFactory:ct(),sessionDescriptionHandlerFactoryOptions:{},sipExtension100rel:J.Unsupported,sipExtensionReplaces:J.Unsupported,sipExtensionExtraSupported:[],sipjsId:"",transportConstructor:ht,transportOptions:{},uri:new w("sip","anonymous","anonymous.invalid"),userAgentString:"SIP.js/0.21.2",viaHost:""}}static newUUID(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(e=>{const t=Math.floor(16*Math.random());return("x"===e?t:t%4+8).toString(16)}))}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}get configuration(){return this.options}get contact(){return this._contact}get instanceId(){return this._instanceId}get state(){return this._state}get stateChange(){return this._stateEventEmitter}get transport(){return this._transport}get userAgentCore(){return this._userAgentCore}getLogger(e,t){return this.loggerFactory.getLogger(e,t)}getLoggerFactory(){return this.loggerFactory}isConnected(){return this.transport.isConnected()}reconnect(){return this.state===ae.Stopped?Promise.reject(new Error("User agent stopped.")):Promise.resolve().then((()=>this.transport.connect()))}start(){return this.state===ae.Started?(this.logger.warn("User agent already started"),Promise.resolve()):(this.logger.log(`Starting ${this.configuration.uri}`),this.transitionState(ae.Started),this.transport.connect())}async stop(){if(this.state===ae.Stopped)return this.logger.warn("User agent already stopped"),Promise.resolve();if(this.logger.log(`Stopping ${this.configuration.uri}`),!this.options.gracefulShutdown)return this.logger.log("Dispose of transport"),this.transport.dispose().catch((e=>{throw this.logger.error(e.message),e})),this.logger.log("Dispose of core"),this.userAgentCore.dispose(),this._publishers={},this._registerers={},this._sessions={},this._subscriptions={},this.transitionState(ae.Stopped),Promise.resolve();const e=Object.assign({},this._publishers),t=Object.assign({},this._registerers),s=Object.assign({},this._sessions),i=Object.assign({},this._subscriptions),r=this.transport,n=this.userAgentCore;this.logger.log("Dispose of registerers");for(const e in t)t[e]&&await t[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._registerers[e],t}));this.logger.log("Dispose of sessions");for(const e in s)s[e]&&await s[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._sessions[e],t}));this.logger.log("Dispose of subscriptions");for(const e in i)i[e]&&await i[e].dispose().catch((t=>{throw this.logger.error(t.message),delete this._subscriptions[e],t}));this.logger.log("Dispose of publishers");for(const t in e)e[t]&&await e[t].dispose().catch((e=>{throw this.logger.error(e.message),delete this._publishers[t],e}));this.logger.log("Dispose of transport"),await r.dispose().catch((e=>{throw this.logger.error(e.message),e})),this.logger.log("Dispose of core"),n.dispose(),this.transitionState(ae.Stopped)}_makeInviter(e,t){return new ee(this,e,t)}attemptReconnection(e=1){const t=this.options.reconnectionAttempts,s=this.options.reconnectionDelay;e>t?this.logger.log("Maximum reconnection attempts reached"):(this.logger.log(`Reconnection attempt ${e} of ${t} - trying`),setTimeout((()=>{this.reconnect().then((()=>{this.logger.log(`Reconnection attempt ${e} of ${t} - succeeded`)})).catch((s=>{this.logger.error(s.message),this.logger.log(`Reconnection attempt ${e} of ${t} - failed`),this.attemptReconnection(++e)}))}),1===e?0:1e3*s))}initContact(){const e=""!==this.options.contactName?this.options.contactName:$(8),t=this.options.contactParams;return{pubGruu:void 0,tempGruu:void 0,uri:new w("sip",e,this.options.viaHost,void 0,t),toString:(e={})=>{const s=e.anonymous||!1,i=e.outbound||!1,r=e.register||!1;let n="<";return n+=s?this.contact.tempGruu||`sip:anonymous@anonymous.invalid;transport=${t.transport?t.transport:"ws"}`:r?this.contact.uri:this.contact.pubGruu||this.contact.uri,i&&(n+=";ob"),n+=">",this.options.instanceIdAlwaysAdded&&(n+=';+sip.instance=""'),n}}}initCore(){let e=[];e.push("outbound"),this.options.sipExtension100rel===J.Supported&&e.push("100rel"),this.options.sipExtensionReplaces===J.Supported&&e.push("replaces"),this.options.sipExtensionExtraSupported&&e.push(...this.options.sipExtensionExtraSupported),this.options.hackAllowUnregisteredOptionTags||(e=e.filter((e=>X[e]))),e=Array.from(new Set(e));const t=e.slice();(this.contact.pubGruu||this.contact.tempGruu)&&t.push("gruu");const s={aor:this.options.uri,contact:this.contact,displayName:this.options.displayName,loggerFactory:this.loggerFactory,hackViaTcp:this.options.hackViaTcp,routeSet:this.options.preloadedRouteSet,supportedOptionTags:e,supportedOptionTagsResponse:t,sipjsId:this.options.sipjsId,userAgentHeaderFieldValue:this.options.userAgentString,viaForceRport:this.options.forceRport,viaHost:this.options.viaHost,authenticationFactory:()=>{const e=this.options.authorizationUsername?this.options.authorizationUsername:this.options.uri.user,t=this.options.authorizationPassword?this.options.authorizationPassword:void 0,s=this.options.authorizationHa1?this.options.authorizationHa1:void 0;return new we(this.getLoggerFactory(),s,e,t)},transportAccessor:()=>this.transport};return new rt(s,{onInvite:e=>{var t;const s=new Q(this,e);if(e.delegate={onCancel:e=>{s._onCancel(e)},onTransportError:e=>{this.logger.error("A transport error has occurred while handling an incoming INVITE request.")}},e.trying(),this.options.sipExtensionReplaces!==J.Unsupported){const t=e.message.parseHeader("replaces");if(t){const e=t.call_id;if("string"!=typeof e)throw new Error("Type of call id is not string");const i=t.replaces_to_tag;if("string"!=typeof i)throw new Error("Type of to tag is not string");const r=t.replaces_from_tag;if("string"!=typeof r)throw new Error("type of from tag is not string");const n=e+i+r,o=this.userAgentCore.dialogs.get(n);if(!o)return void s.reject({statusCode:481});if(!o.early&&!0===t.early_only)return void s.reject({statusCode:486});const a=this._sessions[e+r]||this._sessions[e+i]||void 0;if(!a)throw new Error("Session does not exist.");s._replacee=a}}if(null===(t=this.delegate)||void 0===t?void 0:t.onInvite)return s.autoSendAnInitialProvisionalResponse?void s.progress().then((()=>{var e;if(void 0===(null===(e=this.delegate)||void 0===e?void 0:e.onInvite))throw new Error("onInvite undefined.");this.delegate.onInvite(s)})):void this.delegate.onInvite(s);s.reject({statusCode:486})},onMessage:e=>{if(this.delegate&&this.delegate.onMessage){const t=new W(e);this.delegate.onMessage(t)}else e.accept()},onNotify:e=>{if(this.delegate&&this.delegate.onNotify){const t=new K(e);this.delegate.onNotify(t)}else this.options.allowLegacyNotifications?e.accept():e.reject({statusCode:481})},onRefer:e=>{this.logger.warn("Received an out of dialog REFER request"),this.delegate&&this.delegate.onReferRequest?this.delegate.onReferRequest(e):e.reject({statusCode:405})},onRegister:e=>{this.logger.warn("Received an out of dialog REGISTER request"),this.delegate&&this.delegate.onRegisterRequest?this.delegate.onRegisterRequest(e):e.reject({statusCode:405})},onSubscribe:e=>{this.logger.warn("Received an out of dialog SUBSCRIBE request"),this.delegate&&this.delegate.onSubscribeRequest?this.delegate.onSubscribeRequest(e):e.reject({statusCode:405})}})}initTransportCallbacks(){this.transport.onConnect=()=>this.onTransportConnect(),this.transport.onDisconnect=e=>this.onTransportDisconnect(e),this.transport.onMessage=e=>this.onTransportMessage(e)}onTransportConnect(){this.state!==ae.Stopped&&this.delegate&&this.delegate.onConnect&&this.delegate.onConnect()}onTransportDisconnect(e){this.state!==ae.Stopped&&(this.delegate&&this.delegate.onDisconnect&&this.delegate.onDisconnect(e),e&&this.options.reconnectionAttempts>0&&this.attemptReconnection())}onTransportMessage(e){const t=he.parseMessage(e,this.getLogger("sip.Parser"));if(!t)return void this.logger.warn("Failed to parse incoming message. Dropping.");if(this.state===ae.Stopped&&t instanceof P)return void this.logger.warn(`Received ${t.method} request while stopped. Dropping.`);const s=()=>{const e=["from","to","call_id","cseq","via"];for(const s of e)if(!t.hasHeader(s))return this.logger.warn(`Missing mandatory header field : ${s}.`),!1;return!0};if(t instanceof P){if(!s())return void this.logger.warn("Request missing mandatory header field. Dropping.");if(!t.toTag&&t.callId.substr(0,5)===this.options.sipjsId)return void this.userAgentCore.replyStateless(t,{statusCode:482});const e=D(t.body),i=t.getHeader("content-length");if(i&&e1)return void this.logger.warn("More than one Via header field present in the response. Dropping.");if(t.via.host!==this.options.viaHost||void 0!==t.via.port)return void this.logger.warn("Via sent-by in the response does not match UA Via host value. Dropping.");const e=D(t.body),i=t.getHeader("content-length");if(i&&e{throw new Error(`Invalid state transition from ${this._state} to ${e}`)};switch(this._state){case ae.Started:e!==ae.Stopped&&s();break;case ae.Stopped:e!==ae.Started&&s();break;default:throw new Error("Unknown state.")}this.logger.log(`Transitioned from ${this._state} to ${e}`),this._state=e,this._stateEventEmitter.emit(this._state)}}class lt extends He{constructor(e,t,s){super(Ce,e,t,s)}}class gt extends ke{constructor(e,t,s){super(xe,e.userAgentCore,t,s)}}const ut=(e,t)=>{const s=[],i=e.split(/\r\n/);let r;for(let e=0;e(t.sdp=ut(t.sdp||"",e),Promise.resolve(t))}function bt(e){return e.sdp=((e,t)=>{const s=new RegExp("m="+t+".*$","gm"),i=new RegExp("^a=group:.*$","gm");if(s.test(e)){let s;const r=(e=e.split(/^m=/gm).filter((e=>{if(e.substr(0,t.length)===t){if(s=e.match(/^a=mid:.*$/gm),s){const e=s[0].match(/:.+$/g);e&&(s=e[0].substr(1))}return!1}return!0})).join("m=")).match(i);if(r&&1===r.length){let t=r[0];const n=new RegExp(" *"+s+"[^ ]*","g");t=t.replace(n,""),e=e.split(i).join(t)}}return e})(e.sdp||"","video"),Promise.resolve(e)}function Tt(e){let t=e.sdp||"";if(-1===t.search(/^a=mid.*$/gm)){const s=t.match(/^m=.*$/gm),i=t.split(/^m=.*$/gm);s&&s.forEach(((e,t)=>{s[t]=e+"\na=mid:"+t})),i.forEach(((e,t)=>{s&&s[t]&&(i[t]=e+s[t])})),t=i.join(""),e.sdp=t}return Promise.resolve(e)}function St(e){if(!e.sdp||!e.type)throw new Error("Invalid SDP");let t=e.sdp;const s=e.type;return t&&(/a=(sendrecv|sendonly|recvonly|inactive)/.test(t)?(t=t.replace(/a=sendrecv\r\n/g,"a=sendonly\r\n"),t=t.replace(/a=recvonly\r\n/g,"a=inactive\r\n")):t=t.replace(/(m=[^\r]*\r\n)/g,"$1a=sendonly\r\n")),Promise.resolve({sdp:t,type:s})}function Rt(e){if(e.length<2)throw new Error("Start local conference requires at leaast 2 sessions.");var t;(t=e.map((e=>e.sessionDescriptionHandler)),t.map(((e,s)=>t.slice(s+1).map((t=>[e,t])))).reduce(((e,t)=>e.concat(t)),[])).forEach((([e,t])=>{if(!(e instanceof yt&&t instanceof yt))throw new Error("Session description handler not instance of SessionManagerSessionDescriptionHandler");e.joinWith(t)}))}class yt extends at{constructor(e,t,s){super(e,t,s),yt.audioContext||(yt.audioContext=new AudioContext)}enableSenderTracks(e){const t=this.localMediaStreamReal;if(void 0===t)throw new Error("Stream undefined.");t.getAudioTracks().forEach((t=>{t.enabled=e}))}initLocalMediaStream(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");return this.localMediaStreamReal=e,this.localMediaStreamSourceNode=yt.audioContext.createMediaStreamSource(e),this.localMediaStreamDestinationNode=yt.audioContext.createMediaStreamDestination(),this.localMediaStreamSourceNode.connect(this.localMediaStreamDestinationNode),this.localMediaStreamDestinationNode.stream}joinWith(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");const t=yt.audioContext.createMediaStreamSource(this.remoteMediaStream),s=e.localMediaStreamDestinationNode;if(void 0===s)throw new Error("Peer outbound (local) stream local media stream destination is undefined.");t.connect(s);const i=yt.audioContext.createMediaStreamSource(e.remoteMediaStream),r=this.localMediaStreamDestinationNode;if(void 0===r)throw new Error("Our outbound (local) stream local media stream destination is undefined.");i.connect(r)}setRealLocalMediaStream(e){if(!yt.audioContext)throw new Error("SessionManagerSessionDescriptionHandler.audioContext undefined.");if(this.localMediaStreamReal){if(!this.localMediaStreamDestinationNode||!this.localMediaStreamSourceNode||!this.localMediaStreamReal)throw new Error("Local media stream undefined.");this.localMediaStreamReal=e,this.localMediaStreamSourceNode.disconnect(this.localMediaStreamDestinationNode),this.localMediaStreamSourceNode=yt.audioContext.createMediaStreamSource(e),this.localMediaStreamSourceNode.connect(this.localMediaStreamDestinationNode)}else this.initLocalMediaStream(e)}}function Et(){return(e,t)=>({session:t,held:!1,muted:!1})}class $t{constructor(e,t={}){this.managedSessions=[],this.attemptingReconnection=!1,this.optionsPingFailure=!1,this.optionsPingRunning=!1,this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.delegate=t.delegate,this.options=Object.assign({aor:"",autoStop:!0,delegate:{},iceStopWaitingOnServerReflexive:!1,managedSessionFactory:(e,t)=>({session:t,held:!1,muted:!1}),maxSimultaneousSessions:2,media:{},optionsPingInterval:-1,optionsPingRequestURI:"",reconnectionAttempts:3,reconnectionDelay:4,registrationRetry:!1,registrationRetryInterval:3,registerGuard:null,registererOptions:{},registererRegisterOptions:{},sendDTMFUsingSessionDescriptionHandler:!1,userAgentOptions:{}},$t.stripUndefinedProperties(t));const s=Object.assign({},t.userAgentOptions);if(s.transportConstructor||(s.transportConstructor=ht),s.transportOptions||(s.transportOptions={server:e}),!s.uri&&t.aor){const e=dt.makeURI(t.aor);if(!e)throw new Error(`Failed to create valid URI from ${t.aor}`);s.uri=e}if(this.userAgent=new dt(s),this.userAgent.delegate={onConnect:()=>{this.logger.log("Connected"),this.delegate&&this.delegate.onServerConnect&&this.delegate.onServerConnect(),this.shouldBeRegistered&&this.register(),this.options.optionsPingInterval>0&&this.optionsPingStart()},onDisconnect:async e=>{this.logger.log("Disconnected");let t=!1;this.options.optionsPingInterval>0&&(t=this.optionsPingFailure,this.optionsPingFailure=!1,this.optionsPingStop()),this.delegate&&this.delegate.onServerDisconnect&&this.delegate.onServerDisconnect(e),(e||t)&&(this.registerer&&(this.logger.log("Disposing of registerer..."),this.registerer.dispose().catch((e=>{this.logger.debug("Error occurred disposing of registerer after connection with server was lost."),this.logger.debug(e.toString())})),this.registerer=void 0),this.managedSessions.slice().map((e=>e.session)).forEach((async e=>{this.logger.log("Disposing of session..."),e.dispose().catch((e=>{this.logger.debug("Error occurred disposing of a session after connection with server was lost."),this.logger.debug(e.toString())}))})),this.shouldBeConnected&&this.attemptReconnection())},onInvite:e=>{this.logger.log(`[${e.id}] Received INVITE`);const t=this.options.maxSimultaneousSessions;if(0!==t&&this.managedSessions.length>t)return this.logger.warn(`[${e.id}] Session already in progress, rejecting INVITE...`),void e.reject().then((()=>{this.logger.log(`[${e.id}] Rejected INVITE`)})).catch((t=>{this.logger.error(`[${e.id}] Failed to reject INVITE`),this.logger.error(t.toString())}));const s={sessionDescriptionHandlerOptions:{constraints:this.constraints}};this.initSession(e,s),this.delegate&&this.delegate.onCallReceived?this.delegate.onCallReceived(e):(this.logger.warn(`[${e.id}] No handler available, rejecting INVITE...`),e.reject().then((()=>{this.logger.log(`[${e.id}] Rejected INVITE`)})).catch((t=>{this.logger.error(`[${e.id}] Failed to reject INVITE`),this.logger.error(t.toString())})))},onMessage:e=>{e.accept().then((()=>{this.delegate&&this.delegate.onMessageReceived&&this.delegate.onMessageReceived(e)}))},onNotify:e=>{e.accept().then((()=>{this.delegate&&this.delegate.onNotificationReceived&&this.delegate.onNotificationReceived(e)}))}},this.registererOptions=Object.assign({},t.registererOptions),this.registererRegisterOptions=Object.assign({},t.registererRegisterOptions),this.options.registrationRetry){this.registererRegisterOptions.requestDelegate=this.registererRegisterOptions.requestDelegate||{};const e=this.registererRegisterOptions.requestDelegate.onReject;this.registererRegisterOptions.requestDelegate.onReject=t=>{e&&e(t),this.attemptRegistration()}}this.logger=this.userAgent.getLogger("sip.SessionManager"),window.addEventListener("online",(()=>{this.logger.log("Online"),this.shouldBeConnected&&this.connect()})),this.options.autoStop&&window.addEventListener("beforeunload",(async()=>{this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.userAgent.state!==ae.Stopped&&await this.userAgent.stop()}))}static stripUndefinedProperties(e){return Object.keys(e).reduce(((t,s)=>(void 0!==e[s]&&(t[s]=e[s]),t)),{})}getLocalMediaStream(e){const t=e.sessionDescriptionHandler;if(t){if(!(t instanceof at))throw new Error("Session description handler not instance of web SessionDescriptionHandler");return t.localMediaStream}}getRemoteMediaStream(e){const t=e.sessionDescriptionHandler;if(t){if(!(t instanceof at))throw new Error("Session description handler not instance of web SessionDescriptionHandler");return t.remoteMediaStream}}getLocalAudioTrack(e){var t;return null===(t=this.getLocalMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"audio"===e.kind))}getLocalVideoTrack(e){var t;return null===(t=this.getLocalMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"video"===e.kind))}getRemoteAudioTrack(e){var t;return null===(t=this.getRemoteMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"audio"===e.kind))}getRemoteVideoTrack(e){var t;return null===(t=this.getRemoteMediaStream(e))||void 0===t?void 0:t.getTracks().find((e=>"video"===e.kind))}async connect(){return this.logger.log("Connecting UserAgent..."),this.shouldBeConnected=!0,this.userAgent.state!==ae.Started?this.userAgent.start():this.userAgent.reconnect()}async disconnect(){return this.logger.log("Disconnecting UserAgent..."),this.userAgent.state===ae.Stopped?Promise.resolve():(this.shouldBeConnected=!1,this.shouldBeRegistered=!1,this.registerer=void 0,this.userAgent.stop())}isConnected(){return this.userAgent.isConnected()}async register(e){return this.logger.log("Registering UserAgent..."),this.shouldBeRegistered=!0,void 0!==e&&(this.registererRegisterOptions=Object.assign({},e)),this.registerer||(this.registerer=new ge(this.userAgent,this.registererOptions),this.registerer.stateChange.addListener((e=>{switch(e){case ie.Initial:break;case ie.Registered:this.delegate&&this.delegate.onRegistered&&this.delegate.onRegistered();break;case ie.Unregistered:this.delegate&&this.delegate.onUnregistered&&this.delegate.onUnregistered(),this.shouldBeRegistered&&this.attemptRegistration();break;case ie.Terminated:break;default:throw new Error("Unknown registerer state.")}}))),this.attemptRegistration(!0)}async unregister(e){return this.logger.log("Unregistering UserAgent..."),this.shouldBeRegistered=!1,this.registerer?this.registerer.unregister(e).then((()=>{})):(this.logger.warn("No registerer to unregister."),Promise.resolve())}async call(e,t,s){this.logger.log("Beginning Session...");const i=this.options.maxSimultaneousSessions;if(0!==i&&this.managedSessions.length>i)return Promise.reject(new Error("Maximum number of sessions already exists."));const r=dt.makeURI(e);if(!r)return Promise.reject(new Error(`Failed to create a valid URI from "${e}"`));if(t||(t={}),t.sessionDescriptionHandlerOptions||(t.sessionDescriptionHandlerOptions={}),t.sessionDescriptionHandlerOptions.constraints||(t.sessionDescriptionHandlerOptions.constraints=this.constraints),t.earlyMedia){(s=s||{}).requestDelegate=s.requestDelegate||{};const e=s.requestDelegate.onProgress;s.requestDelegate.onProgress=t=>{183===t.message.statusCode&&this.setupRemoteMedia(n),e&&e(t)}}this.options.iceStopWaitingOnServerReflexive&&(t.delegate=t.delegate||{},t.delegate.onSessionDescriptionHandler=e=>{if(!(e instanceof at))throw new Error("Session description handler not instance of SessionDescriptionHandler");e.peerConnectionDelegate={onicecandidate:t=>{var s;if("srflx"===(null===(s=t.candidate)||void 0===s?void 0:s.type)){this.logger.log(`[${n.id}] Found srflx ICE candidate, stop waiting...`);e.iceGatheringComplete()}}}});const n=new ee(this.userAgent,r,t);return this.sendInvite(n,t,s).then((()=>n))}async hangup(e){return this.logger.log(`[${e.id}] Hangup...`),this.sessionExists(e)?this.terminate(e):Promise.reject(new Error("Session does not exist."))}async answer(e,t){return this.logger.log(`[${e.id}] Accepting Invitation...`),this.sessionExists(e)?e instanceof Q?(t||(t={}),t.sessionDescriptionHandlerOptions||(t.sessionDescriptionHandlerOptions={}),t.sessionDescriptionHandlerOptions.constraints||(t.sessionDescriptionHandlerOptions.constraints=this.constraints),e.accept(t)):Promise.reject(new Error("Session not instance of Invitation.")):Promise.reject(new Error("Session does not exist."))}async decline(e){return this.logger.log(`[${e.id}] Rejecting Invitation...`),this.sessionExists(e)?e instanceof Q?e.reject():Promise.reject(new Error("Session not instance of Invitation.")):Promise.reject(new Error("Session does not exist."))}async hold(e){return this.logger.log(`[${e.id}] Holding session...`),this.setHold(e,!0)}async unhold(e){return this.logger.log(`[${e.id}] Unholding session...`),this.setHold(e,!1)}isHeld(e){const t=this.sessionManaged(e);return!!t&&t.held}mute(e){this.logger.log(`[${e.id}] Disabling media tracks...`),this.setMute(e,!0)}unmute(e){this.logger.log(`[${e.id}] Enabling media tracks...`),this.setMute(e,!1)}isMuted(e){const t=this.sessionManaged(e);return!!t&&t.muted}async sendDTMF(e,t){if(this.logger.log(`[${e.id}] Sending DTMF...`),!/^[0-9A-D#*,]$/.exec(t))return Promise.reject(new Error("Invalid DTMF tone."));if(!this.sessionExists(e))return Promise.reject(new Error("Session does not exist."));if(this.logger.log(`[${e.id}] Sending DTMF tone: ${t}`),this.options.sendDTMFUsingSessionDescriptionHandler)return e.sessionDescriptionHandler?e.sessionDescriptionHandler.sendDtmf(t)?Promise.resolve():Promise.reject(new Error("Failed to send DTMF")):Promise.reject(new Error("Session desciption handler undefined."));{const s={body:{contentDisposition:"render",contentType:"application/dtmf-relay",content:"Signal="+t+"\r\nDuration="+2e3}};return e.info({requestOptions:s}).then((()=>{}))}}async transfer(e,t,s){if(this.logger.log(`[${e.id}] Referring session...`),t instanceof z)return e.refer(t,s).then((()=>{}));const i=dt.makeURI(t);return i?e.refer(i,s).then((()=>{})):Promise.reject(new Error(`Failed to create a valid URI from "${t}"`))}async message(e,t){this.logger.log("Sending message...");const s=dt.makeURI(e);return s?new te(this.userAgent,s,t).message():Promise.reject(new Error(`Failed to create a valid URI from "${e}"`))}get constraints(){let e={audio:!0,video:!1};return this.options.media.constraints&&(e=Object.assign({},this.options.media.constraints)),e}attemptReconnection(e=1){const t=this.options.reconnectionAttempts,s=this.options.reconnectionDelay;this.shouldBeConnected?(this.attemptingReconnection&&this.logger.log("Reconnection attempt already in progress"),e>t?this.logger.log("Reconnection maximum attempts reached"):(1===e?this.logger.log(`Reconnection attempt ${e} of ${t} - trying`):this.logger.log(`Reconnection attempt ${e} of ${t} - trying in ${s} seconds`),this.attemptingReconnection=!0,setTimeout((()=>{if(!this.shouldBeConnected)return this.logger.log(`Reconnection attempt ${e} of ${t} - aborted`),void(this.attemptingReconnection=!1);this.userAgent.reconnect().then((()=>{this.logger.log(`Reconnection attempt ${e} of ${t} - succeeded`),this.attemptingReconnection=!1})).catch((s=>{this.logger.log(`Reconnection attempt ${e} of ${t} - failed`),this.logger.error(s.message),this.attemptingReconnection=!1,this.attemptReconnection(++e)}))}),1===e?0:1e3*s))):this.logger.log("Should not be connected currently")}attemptRegistration(e=!1){if(this.logger.log("Registration attempt "+(e?"without delay":"")),!this.shouldBeRegistered)return this.logger.log("Should not be registered currently"),Promise.resolve();if(void 0!==this.registrationAttemptTimeout)return this.logger.log("Registration attempt already in progress"),Promise.resolve();const t=()=>this.registerer?this.isConnected()?this.userAgent.state===ae.Stopped?(this.logger.log("User agent stopped"),Promise.resolve()):this.options.registerGuard?this.options.registerGuard().catch((e=>{throw this.logger.log("Register guard rejected will making registration attempt"),e})).then((e=>e||!this.registerer?Promise.resolve():this.registerer.register(this.registererRegisterOptions).then((()=>{})))):this.registerer.register(this.registererRegisterOptions).then((()=>{})):(this.logger.log("User agent not connected"),Promise.resolve()):(this.logger.log("Registerer undefined"),Promise.resolve());return new Promise(((s,i)=>{this.registrationAttemptTimeout=setTimeout((()=>{t().then((()=>{this.registrationAttemptTimeout=void 0,s()})).catch((e=>{this.registrationAttemptTimeout=void 0,e instanceof a?s():i(e)}))}),e?0:(e=>{const t=2*e;return 1e3*(Math.random()*(t-e)+e)})(this.options.registrationRetryInterval))}))}cleanupMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");t.mediaLocal&&t.mediaLocal.video&&(t.mediaLocal.video.srcObject=null,t.mediaLocal.video.pause()),t.mediaRemote&&(t.mediaRemote.audio&&(t.mediaRemote.audio.srcObject=null,t.mediaRemote.audio.pause()),t.mediaRemote.video&&(t.mediaRemote.video.srcObject=null,t.mediaRemote.video.pause()))}enableReceiverTracks(e,t){if(!this.sessionExists(e))throw new Error("Session does not exist.");const s=e.sessionDescriptionHandler;if(!(s instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");s.enableReceiverTracks(t)}enableSenderTracks(e,t){if(!this.sessionExists(e))throw new Error("Session does not exist.");const s=e.sessionDescriptionHandler;if(!(s instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");s.enableSenderTracks(t)}initSession(e,t){this.sessionAdd(e),this.delegate&&this.delegate.onCallCreated&&this.delegate.onCallCreated(e),e.stateChange.addListener((t=>{switch(this.logger.log(`[${e.id}] Session state changed to ${t}`),t){case Z.Initial:case Z.Establishing:break;case Z.Established:this.setupLocalMedia(e),this.setupRemoteMedia(e),this.delegate&&this.delegate.onCallAnswered&&this.delegate.onCallAnswered(e);break;case Z.Terminating:case Z.Terminated:this.sessionExists(e)&&(this.cleanupMedia(e),this.sessionRemove(e),this.delegate&&this.delegate.onCallHangup&&this.delegate.onCallHangup(e));break;default:throw new Error("Unknown session state.")}})),e.delegate=e.delegate||{},e.delegate.onInfo=t=>{var s;if(void 0===(null===(s=this.delegate)||void 0===s?void 0:s.onCallDTMFReceived))return void t.reject();const i=t.request.getHeader("content-type");if(!i||!/^application\/dtmf-relay/i.exec(i))return void t.reject();const r=t.request.body.split("\r\n",2);if(2!==r.length)return void t.reject();let n;const o=/^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*/;if(void 0!==r[0]&&o.test(r[0])&&(n=r[0].replace(o,"$2")),!n)return void t.reject();let a;const c=/^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*/;void 0!==r[1]&&c.test(r[1])&&(a=parseInt(r[1].replace(c,"$2"),10)),a?t.accept().then((()=>{if(this.delegate&&this.delegate.onCallDTMFReceived){if(!n||!a)throw new Error("Tone or duration undefined.");this.delegate.onCallDTMFReceived(e,n,a)}})).catch((e=>{this.logger.error(e.message)})):t.reject()},e.delegate.onRefer=e=>{e.accept().then((()=>this.sendInvite(e.makeInviter(t),t))).catch((e=>{this.logger.error(e.message)}))}}optionsPingRun(e,t,s){if(this.options.optionsPingInterval<1)throw new Error("Invalid options ping interval.");this.optionsPingRunning||(this.optionsPingRunning=!0,this.optionsPingTimeout=setTimeout((()=>{this.optionsPingTimeout=void 0;const i=()=>{this.optionsPingFailure=!1,this.optionsPingRunning&&(this.optionsPingRunning=!1,this.optionsPingRun(e,t,s))},r=()=>{this.logger.error("OPTIONS ping failed"),this.optionsPingFailure=!0,this.optionsPingRunning=!1,this.userAgent.transport.disconnect().catch((e=>this.logger.error(e)))},n=this.userAgent.userAgentCore,o=n.makeOutgoingRequestMessage("OPTIONS",e,t,s,{});this.optionsPingRequest=n.request(o,{onAccept:()=>{this.optionsPingRequest=void 0,i()},onReject:e=>{this.optionsPingRequest=void 0,408===e.message.statusCode||503===e.message.statusCode?r():i()}})}),1e3*this.options.optionsPingInterval))}optionsPingStart(){let e,t,s;if(this.logger.log("OPTIONS pings started"),this.options.optionsPingRequestURI){if(e=dt.makeURI(this.options.optionsPingRequestURI),!e)throw new Error("Failed to create Request URI.");t=this.userAgent.contact.uri.clone(),s=this.userAgent.contact.uri.clone()}else{if(!this.options.aor)return void this.logger.error("You have enabled sending OPTIONS pings and as such you must provide either a) an AOR to register, or b) an RURI to use for the target of the OPTIONS ping requests. ");{const i=dt.makeURI(this.options.aor);if(!i)throw new Error("Failed to create URI.");e=i.clone(),e.user=void 0,t=i.clone(),s=i.clone()}}this.optionsPingRun(e,t,s)}optionsPingStop(){this.logger.log("OPTIONS pings stopped"),this.optionsPingRunning=!1,this.optionsPingFailure=!1,this.optionsPingRequest&&(this.optionsPingRequest.dispose(),this.optionsPingRequest=void 0),this.optionsPingTimeout&&(clearTimeout(this.optionsPingTimeout),this.optionsPingTimeout=void 0)}async sendInvite(e,t,s){return this.initSession(e,t),e.invite(s).then((()=>{this.logger.log(`[${e.id}] Sent INVITE`)}))}sessionAdd(e){const t=this.options.managedSessionFactory(this,e);this.managedSessions.push(t)}sessionExists(e){return void 0!==this.sessionManaged(e)}sessionManaged(e){return this.managedSessions.find((t=>t.session.id===e.id))}sessionRemove(e){this.managedSessions=this.managedSessions.filter((t=>t.session.id!==e.id))}async setHold(e,t){if(!this.sessionExists(e))return Promise.reject(new Error("Session does not exist."));if(this.isHeld(e)===t)return Promise.resolve();if(!(e.sessionDescriptionHandler instanceof at))throw new Error("Session's session description handler not instance of SessionDescriptionHandler.");const s={requestDelegate:{onAccept:()=>{const s=this.sessionManaged(e);void 0!==s&&(s.held=t,this.enableReceiverTracks(e,!s.held),this.enableSenderTracks(e,!s.held&&!s.muted),this.delegate&&this.delegate.onCallHold&&this.delegate.onCallHold(e,s.held))},onReject:()=>{this.logger.warn(`[${e.id}] Re-invite request was rejected`);const s=this.sessionManaged(e);void 0!==s&&(s.held=!t,this.enableReceiverTracks(e,!s.held),this.enableSenderTracks(e,!s.held&&!s.muted),this.delegate&&this.delegate.onCallHold&&this.delegate.onCallHold(e,s.held))}}},i=e.sessionDescriptionHandlerOptionsReInvite;i.hold=t,e.sessionDescriptionHandlerOptionsReInvite=i;const r=this.sessionManaged(e);if(!r)throw new Error("Managed session is undefiend.");return r.held=t,e.invite(s).then((()=>{const t=this.sessionManaged(e);void 0!==t&&(this.enableReceiverTracks(e,!t.held),this.enableSenderTracks(e,!t.held&&!t.muted))})).catch((s=>{throw r.held=!t,s instanceof a&&this.logger.error(`[${e.id}] A hold request is already in progress.`),s}))}setMute(e,t){if(!this.sessionExists(e))return void this.logger.warn(`[${e.id}] A session is required to enabled/disable media tracks`);if(e.state!==Z.Established)return void this.logger.warn(`[${e.id}] An established session is required to enable/disable media tracks`);const s=this.sessionManaged(e);void 0!==s&&(s.muted=t,this.enableSenderTracks(e,!s.held&&!s.muted))}setupLocalMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");const s="function"==typeof this.options.media.local?this.options.media.local(e):this.options.media.local;t.mediaLocal=s;const i=null==s?void 0:s.video;if(i){const t=this.getLocalMediaStream(e);if(!t)throw new Error("Local media stream undefiend.");i.srcObject=t,i.volume=0,i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play local media`),this.logger.error(t.message)}))}}setupRemoteMedia(e){const t=this.sessionManaged(e);if(!t)throw new Error("Managed session does not exist.");const s="function"==typeof this.options.media.remote?this.options.media.remote(e):this.options.media.remote;t.mediaRemote=s;const i=(null==s?void 0:s.video)||(null==s?void 0:s.audio);if(i){const t=this.getRemoteMediaStream(e);if(!t)throw new Error("Remote media stream undefiend.");i.autoplay=!0,i.srcObject=t,i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play remote media`),this.logger.error(t.message)})),t.onaddtrack=()=>{this.logger.log("Remote media onaddtrack"),i.load(),i.play().catch((t=>{this.logger.error(`[${e.id}] Failed to play remote media`),this.logger.error(t.message)}))}}}async terminate(e){switch(this.logger.log(`[${e.id}] Terminating...`),e.state){case Z.Initial:if(e instanceof ee)return e.cancel().then((()=>{this.logger.log(`[${e.id}] Inviter never sent INVITE (canceled)`)}));if(e instanceof Q)return e.reject().then((()=>{this.logger.log(`[${e.id}] Invitation rejected (sent 480)`)}));throw new Error("Unknown session type.");case Z.Establishing:if(e instanceof ee)return e.cancel().then((()=>{this.logger.log(`[${e.id}] Inviter canceled (sent CANCEL)`)}));if(e instanceof Q)return e.reject().then((()=>{this.logger.log(`[${e.id}] Invitation rejected (sent 480)`)}));throw new Error("Unknown session type.");case Z.Established:return e.bye().then((()=>{this.logger.log(`[${e.id}] Session ended (sent BYE)`)}));case Z.Terminating:case Z.Terminated:break;default:throw new Error("Unknown state")}return this.logger.log(`[${e.id}] Terminating in state ${e.state}, no action taken`),Promise.resolve()}}class It{constructor(e,t={}){this.session=void 0,this.delegate=t.delegate,this.options=Object.assign({},t);const s={aor:this.options.aor,delegate:{onCallAnswered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onCallAnswered)||void 0===t?void 0:t.call(e)},onCallCreated:e=>{var t,s;this.session=e,null===(s=null===(t=this.delegate)||void 0===t?void 0:t.onCallCreated)||void 0===s||s.call(t)},onCallReceived:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onCallReceived)||void 0===t?void 0:t.call(e)},onCallHangup:()=>{var e,t;this.session=void 0,(null===(e=this.delegate)||void 0===e?void 0:e.onCallHangup)&&(null===(t=this.delegate)||void 0===t||t.onCallHangup())},onCallHold:(e,t)=>{var s,i;return null===(i=null===(s=this.delegate)||void 0===s?void 0:s.onCallHold)||void 0===i?void 0:i.call(s,t)},onCallDTMFReceived:(e,t,s)=>{var i,r;return null===(r=null===(i=this.delegate)||void 0===i?void 0:i.onCallDTMFReceived)||void 0===r?void 0:r.call(i,t,s)},onMessageReceived:e=>{var t,s;return null===(s=null===(t=this.delegate)||void 0===t?void 0:t.onMessageReceived)||void 0===s?void 0:s.call(t,e.request.body)},onRegistered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onRegistered)||void 0===t?void 0:t.call(e)},onUnregistered:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onUnregistered)||void 0===t?void 0:t.call(e)},onServerConnect:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onServerConnect)||void 0===t?void 0:t.call(e)},onServerDisconnect:()=>{var e,t;return null===(t=null===(e=this.delegate)||void 0===e?void 0:e.onServerDisconnect)||void 0===t?void 0:t.call(e)}},maxSimultaneousSessions:1,media:this.options.media,reconnectionAttempts:this.options.reconnectionAttempts,reconnectionDelay:this.options.reconnectionDelay,registererOptions:this.options.registererOptions,sendDTMFUsingSessionDescriptionHandler:this.options.sendDTMFUsingSessionDescriptionHandler,userAgentOptions:this.options.userAgentOptions};this.sessionManager=new $t(e,s),this.logger=this.sessionManager.userAgent.getLogger("sip.SimpleUser")}get id(){return this.options.userAgentOptions&&this.options.userAgentOptions.displayName||"Anonymous"}get localMediaStream(){return this.session&&this.sessionManager.getLocalMediaStream(this.session)}get remoteMediaStream(){return this.session&&this.sessionManager.getRemoteMediaStream(this.session)}get localAudioTrack(){return this.session&&this.sessionManager.getLocalAudioTrack(this.session)}get localVideoTrack(){return this.session&&this.sessionManager.getLocalVideoTrack(this.session)}get remoteAudioTrack(){return this.session&&this.sessionManager.getRemoteAudioTrack(this.session)}get remoteVideoTrack(){return this.session&&this.sessionManager.getRemoteVideoTrack(this.session)}connect(){return this.logger.log(`[${this.id}] Connecting UserAgent...`),this.sessionManager.connect()}disconnect(){return this.logger.log(`[${this.id}] Disconnecting UserAgent...`),this.sessionManager.disconnect()}isConnected(){return this.sessionManager.isConnected()}register(e){return this.logger.log(`[${this.id}] Registering UserAgent...`),this.sessionManager.register(e)}unregister(e){return this.logger.log(`[${this.id}] Unregistering UserAgent...`),this.sessionManager.unregister(e)}call(e,t,s){return this.logger.log(`[${this.id}] Beginning Session...`),this.session?Promise.reject(new Error("Session already exists.")):this.sessionManager.call(e,t,s).then((()=>{}))}hangup(){return this.logger.log(`[${this.id}] Hangup...`),this.session?this.sessionManager.hangup(this.session).then((()=>{this.session=void 0})):Promise.reject(new Error("Session does not exist."))}answer(e){return this.logger.log(`[${this.id}] Accepting Invitation...`),this.session?this.sessionManager.answer(this.session,e):Promise.reject(new Error("Session does not exist."))}decline(){return this.logger.log(`[${this.id}] rejecting Invitation...`),this.session?this.sessionManager.decline(this.session):Promise.reject(new Error("Session does not exist."))}hold(){return this.logger.log(`[${this.id}] holding session...`),this.session?this.sessionManager.hold(this.session):Promise.reject(new Error("Session does not exist."))}unhold(){return this.logger.log(`[${this.id}] unholding session...`),this.session?this.sessionManager.unhold(this.session):Promise.reject(new Error("Session does not exist."))}isHeld(){return!!this.session&&this.sessionManager.isHeld(this.session)}mute(){return this.logger.log(`[${this.id}] disabling media tracks...`),this.session&&this.sessionManager.mute(this.session)}unmute(){return this.logger.log(`[${this.id}] enabling media tracks...`),this.session&&this.sessionManager.unmute(this.session)}isMuted(){return!!this.session&&this.sessionManager.isMuted(this.session)}sendDTMF(e){return this.logger.log(`[${this.id}] sending DTMF...`),this.session?this.sessionManager.sendDTMF(this.session,e):Promise.reject(new Error("Session does not exist."))}message(e,t){return this.logger.log(`[${this.id}] sending message...`),this.sessionManager.message(e,t)}}const Ct=r,At="sip.js";return t})())); \ No newline at end of file diff --git a/ui/sales/app.js b/ui/sales/app.js new file mode 100644 index 0000000..f25f99b --- /dev/null +++ b/ui/sales/app.js @@ -0,0 +1,927 @@ +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 }); +}); diff --git a/ui/sales/index.html b/ui/sales/index.html new file mode 100644 index 0000000..28c35fb --- /dev/null +++ b/ui/sales/index.html @@ -0,0 +1,215 @@ + + + + + + KonturCC Продажи + + + + + + + + +
            + + +
            +
            + +
            +
            Шлюз: проверка...
            + +
            +
            SL
            +
            +
            Экран продаж
            +
            Omnichannel pipeline
            +
            +
            +
            +
            + +
            +
            +

            Sales Control Room

            +

            Омниканальная воронка продаж

            +

            Теплый лид попадает в CRM, дальше система ведет его через коммуникации, оффер, документы, счет, оплату и post-sale handoff без потери контекста.

            +
            + +
            +

            Сессия

            +
            + + +
            +
            + + +
            +
            + + +
            +
            + +
            +

            Токен: отсутствует

            +
            + +
            + +
            +
            +
            +
            +

            Воронка

            +

            Колонки собраны по крупным бизнес-блокам из ТЗ: вход, коммуникация, коммерция, оформление, финансы и закрытие.

            +
            +
            + +
            +
            +
            +
            + + +
            +
            +
            +
            + + + + diff --git a/ui/sales/styles.css b/ui/sales/styles.css new file mode 100644 index 0000000..1bf90e0 --- /dev/null +++ b/ui/sales/styles.css @@ -0,0 +1,373 @@ +.sales-layout { + gap: 18px; +} + +.sales-dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 14px; +} + +.sales-kpi-card { + padding: 18px; + border-radius: 18px; + border: 1px solid var(--line); + background: + radial-gradient(circle at top right, rgba(51, 102, 232, 0.12), transparent 34%), + linear-gradient(180deg, #ffffff, #f8fbff); + box-shadow: var(--shadow); +} + +.sales-kpi-label { + color: var(--text-soft); + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.sales-kpi-value { + margin-top: 12px; + font-family: "Manrope", sans-serif; + font-size: 30px; + font-weight: 800; + letter-spacing: -0.04em; +} + +.sales-kpi-note { + margin-top: 6px; + color: var(--text-soft); + font-size: 13px; +} + +.sales-main-grid { + display: grid; + grid-template-columns: minmax(0, 1.45fr) minmax(340px, 0.95fr); + gap: 18px; + align-items: start; +} + +.sales-detail-stack { + display: grid; + gap: 18px; +} + +.sales-board-panel, +.sales-detail-panel { + min-height: 320px; +} + +.sales-board-head, +.sales-detail-head { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; +} + +.sales-board-head h2, +.sales-detail-head h2 { + margin-bottom: 8px; +} + +.sales-board { + margin-top: 18px; + display: grid; + grid-template-columns: repeat(6, minmax(220px, 1fr)); + gap: 14px; + overflow-x: auto; + padding-bottom: 6px; +} + +.sales-column { + min-height: 420px; + border: 1px solid var(--line); + border-radius: 20px; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(246, 249, 255, 0.92)); + padding: 14px; + display: grid; + gap: 12px; + align-content: start; +} + +.sales-column-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + +.sales-column-title { + font-family: "Manrope", sans-serif; + font-size: 16px; + font-weight: 800; +} + +.sales-column-count { + min-width: 28px; + height: 28px; + border-radius: 999px; + display: grid; + place-items: center; + background: var(--primary-soft); + border: 1px solid var(--primary-line); + color: var(--primary); + font-size: 13px; + font-weight: 700; +} + +.sales-column-note { + color: var(--text-soft); + font-size: 12px; + line-height: 1.45; +} + +.sales-card-list { + display: grid; + gap: 10px; +} + +.sales-card { + border: 1px solid var(--line); + border-radius: 18px; + background: #fff; + padding: 14px; + display: grid; + gap: 10px; + cursor: pointer; + transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; +} + +.sales-card:hover { + transform: translateY(-2px); + border-color: var(--primary-line); + box-shadow: 0 12px 24px rgba(16, 33, 61, 0.08); +} + +.sales-card.active { + border-color: var(--primary); + box-shadow: 0 16px 30px rgba(51, 102, 232, 0.16); +} + +.sales-card-top { + display: flex; + justify-content: space-between; + gap: 10px; + align-items: flex-start; +} + +.sales-card-title { + font-family: "Manrope", sans-serif; + font-size: 15px; + font-weight: 800; +} + +.sales-card-priority { + font-size: 11px; + font-weight: 700; + color: #9a4f00; + background: #fff2d8; + border-radius: 999px; + padding: 4px 8px; +} + +.sales-card-body { + color: var(--text-soft); + font-size: 13px; + line-height: 1.5; +} + +.sales-card-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sales-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 9px; + border-radius: 999px; + background: #f4f7fd; + color: var(--text-soft); + font-size: 11px; + font-weight: 700; +} + +.sales-workspace-empty { + margin-top: 18px; + border: 1px dashed var(--line-strong); + border-radius: 18px; + padding: 20px; + color: var(--text-soft); + background: #fbfcff; +} + +.sales-quick-actions { + margin-top: 18px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.sales-detail-metrics { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.sales-bind-grid { + margin-top: 14px; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.sales-inline-message { + margin-top: 14px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: start; +} + +.sales-metric { + padding: 12px; + border: 1px solid var(--line); + border-radius: 16px; + background: #fbfdff; +} + +.sales-metric-label { + color: var(--text-soft); + font-size: 12px; +} + +.sales-metric-value { + margin-top: 6px; + font-family: "Manrope", sans-serif; + font-size: 19px; + font-weight: 800; +} + +.sales-workspace-grid { + margin-top: 18px; + display: grid; + gap: 14px; +} + +.sales-section-card { + border: 1px solid var(--line); + border-radius: 18px; + background: #fff; + padding: 14px; +} + +.sales-section-card h3 { + margin: 0 0 10px; +} + +.sales-kv { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.sales-kv-item { + border: 1px solid #edf1f7; + border-radius: 14px; + padding: 10px 12px; + background: #fbfcff; +} + +.sales-kv-label { + color: var(--text-soft); + font-size: 12px; +} + +.sales-kv-value { + margin-top: 6px; + font-size: 14px; + font-weight: 600; +} + +.sales-stream { + display: grid; + gap: 10px; +} + +.sales-stream-item { + border-left: 3px solid var(--primary); + padding: 0 0 0 12px; +} + +.sales-stream-title { + font-weight: 700; +} + +.sales-stream-time { + color: var(--text-faint); + font-size: 12px; + margin-top: 3px; +} + +.sales-stream-body { + color: var(--text-soft); + font-size: 13px; + line-height: 1.5; + margin-top: 5px; +} + +.sales-list { + display: grid; + gap: 8px; +} + +.sales-list-row { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 14px; + background: #f8fbff; + border: 1px solid #edf1f7; +} + +.sales-list-row strong { + display: block; + margin-bottom: 4px; +} + +.sales-list-row span { + color: var(--text-soft); + font-size: 13px; +} + +@media (max-width: 1440px) { + .sales-board { + grid-template-columns: repeat(6, minmax(260px, 1fr)); + } +} + +@media (max-width: 1180px) { + .sales-main-grid { + grid-template-columns: 1fr; + } + + .sales-bind-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .sales-detail-metrics, + .sales-kv { + grid-template-columns: 1fr; + } + + .sales-bind-grid, + .sales-inline-message { + grid-template-columns: 1fr; + } +} diff --git a/ui/supervisor/app.js b/ui/supervisor/app.js new file mode 100644 index 0000000..a654d49 --- /dev/null +++ b/ui/supervisor/app.js @@ -0,0 +1,1027 @@ +const state = { + user: 'supervisor', + role: 'supervisor', + token: null, + authSource: 'local', + fullName: null, + selectedRecordingId: null, + recordingPreviewUrl: null, + logLines: [], + knowledge: { + categories: [], + articles: [], + selectedArticleId: '', + lastQuery: '', + }, + oidc: { + enabled: false, + loginPath: '/auth/oidc/start?return_mode=popup', + providerLabel: 'Keycloak', + }, +}; + +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() { + const allowed = ['admin', 'supervisor']; + if (!allowed.includes(state.role)) { + 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() { + if (state.recordingPreviewUrl) { + URL.revokeObjectURL(state.recordingPreviewUrl); + state.recordingPreviewUrl = null; + } + clearStoredSession(); + window.location.href = '/'; +} + +function syncSessionFromInputs() { + state.user = $('sessionUser').value.trim() || 'supervisor'; + state.role = $('sessionRole').value || 'supervisor'; +} + +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); + $('eventLog').textContent = state.logLines.join('\n'); +} + +function escapeHtml(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function renderSummaryCard(label, value, note = '') { + return ` +
            +
            ${label}
            +
            ${value}
            + ${note ? `
            ${note}
            ` : ''} +
            + `; +} + +function updateProfileMeta() { + const profileName = $('profileName'); + const profileRole = $('profileRole'); + if (!profileName || !profileRole) { + return; + } + profileName.textContent = state.fullName || state.user || 'Пользователь KonturCC'; + 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 fetchBinary(service, path) { + syncSessionFromInputs(); + const headers = { + 'X-User': state.user, + 'X-Role': state.role, + }; + if (state.token) { + headers.Authorization = `Bearer ${state.token}`; + } + const response = await fetch(`/proxy/${service}/${path}`, { + method: 'GET', + headers, + }); + if (!response.ok) { + const detail = await response.text(); + if (response.status === 401 && state.token) { + state.token = null; + state.authSource = 'local'; + clearStoredSession(); + updateSessionInfo('требуется повторный вход'); + window.location.href = '/'; + } + throw new Error(`${response.status}: ${detail}`); + } + return response; +} + +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('Открыт корпоративный вход', { provider: 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 renderRealtime(data) { + const agents = data?.agents || {}; + const byState = agents.by_state || {}; + const firstQueue = (data?.queues || [])[0] || {}; + $('realtimeSummary').innerHTML = [ + renderSummaryCard('Агентов', Number(agents.total || 0), 'Текущее состояние команды'), + renderSummaryCard('Готовы', Number(byState.READY || 0), 'Свободны'), + renderSummaryCard('Заняты', Number(byState.BUSY || 0), 'В обработке'), + renderSummaryCard('Очередь', firstQueue.queue_id || 'нет', `В очереди: ${Number(firstQueue.in_queue || 0)}`), + ].join(''); + $('realtimeOutput').textContent = JSON.stringify(data, null, 2); +} + +function renderKpi(data) { + const kpi = data?.kpi || {}; + $('kpiSummary').innerHTML = [ + renderSummaryCard('SL', `${Number(kpi.SL || 0).toFixed(2)}%`), + renderSummaryCard('ASA', `${Number(kpi.ASA || 0).toFixed(2)} с`), + renderSummaryCard('AHT', `${Number(kpi.AHT || 0).toFixed(2)} с`), + renderSummaryCard('Потери', `${Number(kpi.Abandon || 0).toFixed(2)}%`), + renderSummaryCard('FCR', `${Number(kpi.FCR || 0).toFixed(2)}%`), + ].join(''); + $('kpiOutput').textContent = JSON.stringify(data, null, 2); +} + +function formatTime(ts) { + if (!ts) { + return '-'; + } + const date = new Date(ts); + if (Number.isNaN(date.getTime())) { + return ts; + } + return date.toLocaleString(); +} + +function renderLiveCalls(items) { + if (!items.length) { + $('liveCallsList').innerHTML = '
          • Новых звонков от Asterisk пока нет.
          • '; + return; + } + + $('liveCallsList').innerHTML = items + .map( + (item) => ` +
          • + ${item.call_id}
            + обращение: ${item.interaction_id || '-'}
            + статус: ${item.status}
            + очередь: ${item.queue_id || '-'}
            + начат: ${formatTime(item.started_at)}
            + завершён: ${formatTime(item.ended_at)}
            + запись: ${item.has_recording ? 'да' : 'нет'} +
          • + `, + ) + .join(''); +} + +function kbCategoryName(categoryId) { + const category = state.knowledge.categories.find((item) => item.category_id === categoryId); + return category?.name || categoryId || 'Без категории'; +} + +function kbLanguageLabel(language) { + return String(language || 'ru').toUpperCase(); +} + +function renderKnowledgeSummary() { + const ruCount = state.knowledge.articles.filter((item) => item.language === 'ru').length; + const kzCount = state.knowledge.articles.filter((item) => item.language === 'kz').length; + $('kbSummary').innerHTML = [ + renderSummaryCard('Категорий', state.knowledge.categories.length, 'Структура базы знаний'), + renderSummaryCard('Статей', state.knowledge.articles.length, 'Текущий результат поиска'), + renderSummaryCard('RU', ruCount, 'Русские статьи в текущем срезе'), + renderSummaryCard('KZ', kzCount, 'Казахские статьи в текущем срезе'), + ].join(''); +} + +function syncKnowledgeCategoryOptions(preferredCategoryId = '') { + const select = $('kbArticleCategory'); + const currentValue = preferredCategoryId || select.value; + if (!state.knowledge.categories.length) { + select.innerHTML = ''; + select.value = ''; + return; + } + select.innerHTML = state.knowledge.categories + .map((item) => ``) + .join(''); + const nextValue = state.knowledge.categories.some((item) => item.category_id === currentValue) + ? currentValue + : state.knowledge.categories[0].category_id; + select.value = nextValue; +} + +function renderKnowledgeCategories() { + if (!state.knowledge.categories.length) { + $('kbCategoriesList').innerHTML = '
          • Категории базы знаний пока не созданы.
          • '; + return; + } + $('kbCategoriesList').innerHTML = state.knowledge.categories + .map( + (item) => ` +
          • + ${escapeHtml(item.name)}
            + ID: ${escapeHtml(item.category_id)}
            + описание: ${escapeHtml(item.description || '—')} +
          • + `, + ) + .join(''); +} + +function renderKnowledgeArticlePreview(article = null) { + $('kbArticlePreview').textContent = article ? JSON.stringify(article, null, 2) : 'Выберите статью из списка ниже.'; +} + +function resetKnowledgeArticleForm() { + state.knowledge.selectedArticleId = ''; + $('kbArticleGroupId').value = ''; + $('kbArticleLanguage').value = 'ru'; + $('kbArticleTitle').value = ''; + $('kbArticleTags').value = ''; + $('kbArticleBody').value = ''; + $('saveKbArticleBtn').textContent = 'Сохранить статью'; + $('kbFormHint').textContent = 'Создайте новую статью или выберите существующую для редактирования.'; + syncKnowledgeCategoryOptions(); + renderKnowledgeArticlePreview(null); +} + +function fillKnowledgeArticleForm(article) { + if (!article) { + resetKnowledgeArticleForm(); + return; + } + state.knowledge.selectedArticleId = article.article_id; + $('kbArticleGroupId').value = article.article_group_id || ''; + $('kbArticleLanguage').value = article.language || 'ru'; + $('kbArticleTitle').value = article.title || ''; + $('kbArticleTags').value = Array.isArray(article.tags) ? article.tags.join(', ') : ''; + $('kbArticleBody').value = article.body || ''; + syncKnowledgeCategoryOptions(article.category_id || ''); + $('saveKbArticleBtn').textContent = 'Обновить статью'; + $('kbFormHint').textContent = `Редактирование статьи: ${article.title || article.article_id}`; + renderKnowledgeArticlePreview(article); +} + +function renderKnowledgeArticles() { + renderKnowledgeSummary(); + if (!state.knowledge.articles.length) { + $('kbArticlesList').innerHTML = '
          • Статьи базы знаний не найдены.
          • '; + return; + } + $('kbArticlesList').innerHTML = state.knowledge.articles + .map((item) => { + const tags = Array.isArray(item.tags) && item.tags.length ? item.tags.join(', ') : '—'; + return ` +
          • + ${escapeHtml(item.title)}
            + категория: ${escapeHtml(kbCategoryName(item.category_id))}
            + язык: ${escapeHtml(kbLanguageLabel(item.language))}
            + ID группы: ${escapeHtml(item.article_group_id || item.article_id)}
            + теги: ${escapeHtml(tags)}
            + +
          • + `; + }) + .join(''); +} + +function knowledgeArticlePayloadFromForm() { + return { + category_id: $('kbArticleCategory').value.trim(), + article_group_id: $('kbArticleGroupId').value.trim() || null, + language: $('kbArticleLanguage').value.trim() || 'ru', + title: $('kbArticleTitle').value.trim(), + body: $('kbArticleBody').value.trim(), + tags: $('kbArticleTags').value + .split(',') + .map((item) => item.trim()) + .filter(Boolean), + }; +} + +async function loadKnowledgeCategories(silent = false) { + const data = await api('kb', 'knowledge/categories'); + state.knowledge.categories = Array.isArray(data) ? data : []; + syncKnowledgeCategoryOptions(); + renderKnowledgeCategories(); + renderKnowledgeSummary(); + if (!silent) { + log('Категории базы знаний обновлены', { count: state.knowledge.categories.length }); + } +} + +async function loadKnowledgeArticles(silent = false) { + const query = $('kbSearchQuery').value.trim(); + const params = new URLSearchParams({ q: query, limit: '100' }); + const data = await api('kb', `knowledge/search?${params.toString()}`); + state.knowledge.articles = Array.isArray(data) ? data : []; + state.knowledge.lastQuery = query; + renderKnowledgeArticles(); + if (state.knowledge.selectedArticleId) { + const selected = state.knowledge.articles.find((item) => item.article_id === state.knowledge.selectedArticleId); + if (selected) { + renderKnowledgeArticlePreview(selected); + } + } + if (!silent) { + log('Статьи базы знаний обновлены', { count: state.knowledge.articles.length, query }); + } +} + +async function refreshKnowledgeBase(silent = false) { + try { + await loadKnowledgeCategories(true); + await loadKnowledgeArticles(true); + if (!silent) { + log('База знаний обновлена', { + categories: state.knowledge.categories.length, + articles: state.knowledge.articles.length, + }); + } + } catch (err) { + $('kbCategoriesList').innerHTML = '
          • Не удалось загрузить категории базы знаний.
          • '; + $('kbArticlesList').innerHTML = '
          • Не удалось загрузить статьи базы знаний.
          • '; + $('kbArticlePreview').textContent = `Ошибка базы знаний: ${err.message}`; + if (!silent) { + log('Не удалось обновить базу знаний', { error: err.message }); + } + } +} + +async function createKnowledgeCategory() { + const name = $('kbCategoryName').value.trim(); + const description = $('kbCategoryDescription').value.trim(); + if (!name) { + $('kbFormHint').textContent = 'Укажите название категории перед сохранением.'; + return; + } + try { + const created = await api('kb', 'knowledge/categories', { + method: 'POST', + body: JSON.stringify({ name, description }), + }); + $('kbCategoryName').value = ''; + $('kbCategoryDescription').value = ''; + await loadKnowledgeCategories(true); + syncKnowledgeCategoryOptions(created.category_id); + renderKnowledgeCategories(); + renderKnowledgeSummary(); + $('kbFormHint').textContent = `Категория создана: ${created.name}`; + log('Категория базы знаний создана', { category_id: created.category_id, name: created.name }); + } catch (err) { + $('kbFormHint').textContent = `Не удалось создать категорию: ${err.message}`; + log('Не удалось создать категорию базы знаний', { error: err.message }); + } +} + +async function saveKnowledgeArticle() { + const payload = knowledgeArticlePayloadFromForm(); + if (!payload.category_id) { + $('kbFormHint').textContent = 'Сначала создайте или выберите категорию.'; + return; + } + if (!payload.title || !payload.body) { + $('kbFormHint').textContent = 'Заполните заголовок и текст статьи.'; + return; + } + try { + const articleId = state.knowledge.selectedArticleId; + const data = articleId + ? await api('kb', `knowledge/articles/${encodeURIComponent(articleId)}`, { + method: 'PATCH', + body: JSON.stringify({ + article_group_id: payload.article_group_id, + language: payload.language, + title: payload.title, + body: payload.body, + tags: payload.tags, + }), + }) + : await api('kb', 'knowledge/articles', { + method: 'POST', + body: JSON.stringify(payload), + }); + await loadKnowledgeArticles(true); + fillKnowledgeArticleForm(data); + $('kbFormHint').textContent = articleId ? `Статья обновлена: ${data.title}` : `Статья создана: ${data.title}`; + log(articleId ? 'Статья базы знаний обновлена' : 'Статья базы знаний создана', { article_id: data.article_id, title: data.title }); + } catch (err) { + $('kbFormHint').textContent = `Не удалось сохранить статью: ${err.message}`; + log('Не удалось сохранить статью базы знаний', { error: err.message }); + } +} + +function openKnowledgeArticleFromList(event) { + const button = event.target.closest('[data-kb-article-id]'); + if (!button) { + return; + } + const articleId = button.dataset.kbArticleId || ''; + const article = state.knowledge.articles.find((item) => item.article_id === articleId); + if (!article) { + return; + } + fillKnowledgeArticleForm(article); +} + +async function loadLiveCalls(silent = false) { + try { + const data = await api('supervisor', 'supervisor/live-calls?limit=20'); + const items = Array.isArray(data?.items) ? data.items : []; + renderLiveCalls(items); + if (!silent) { + log('Лента звонков обновлена', { count: items.length }); + } + } catch (err) { + $('liveCallsList').innerHTML = '
          • Не удалось загрузить звонки.
          • '; + if (!silent) { + log('Не удалось обновить ленту звонков', { error: err.message }); + } + } +} + +async function loadRealtime() { + try { + const data = await api('supervisor', 'supervisor/realtime'); + renderRealtime(data); + log('Оперативный срез обновлён', { agents: data?.agents?.total || 0 }); + } catch (err) { + $('realtimeOutput').textContent = err.message; + log('Не удалось обновить оперативный срез', { error: err.message }); + } +} + +async function loadKpi() { + try { + const queueId = encodeURIComponent($('queueMetricId').value.trim() || 'line2'); + const data = await api('reporting', `reports/kpi?queue_id=${queueId}`); + renderKpi(data); + log('KPI обновлены', data?.kpi || {}); + } catch (err) { + $('kpiOutput').textContent = err.message; + log('Не удалось обновить KPI', { error: err.message }); + } +} + +async function loadAgents() { + try { + const data = await api('supervisor', 'supervisor/agents'); + if (!data.length) { + $('agentsList').innerHTML = '
          • Агенты пока не зафиксированы.
          • '; + return; + } + $('agentsList').innerHTML = data + .map((item) => `
          • ${item.agent_id}
            статус: ${item.state}
            очередь: ${item.queue_id || 'не указана'}
          • `) + .join(''); + log('Список агентов обновлён', { count: data.length }); + } catch (err) { + $('agentsList').innerHTML = '
          • Не удалось загрузить агентов.
          • '; + log('Не удалось загрузить агентов', { error: err.message }); + } +} + +async function setAgentState() { + try { + const data = await api('supervisor', 'supervisor/agent-states', { + method: 'POST', + body: JSON.stringify({ + agent_id: $('agentId').value.trim() || 'supervisor_demo_agent', + state: $('agentState').value, + queue_id: $('agentQueueId').value.trim() || null, + }), + }); + log('Статус агента обновлён', { agent_id: data.agent_id, state: data.state }); + await Promise.all([loadAgents(), loadRealtime()]); + } catch (err) { + log('Не удалось обновить статус агента', { error: err.message }); + } +} + +async function saveQueueMetrics() { + try { + const queueId = encodeURIComponent($('queueMetricId').value.trim() || 'line2'); + const inQueue = Number($('queueInQueue').value || 0); + const avgWait = Number($('queueAvgWait').value || 0); + const data = await api( + 'supervisor', + `supervisor/queue-metrics?queue_id=${queueId}&in_queue=${inQueue}&avg_wait_seconds=${avgWait}`, + { method: 'POST' }, + ); + $('queueMetricsOutput').textContent = JSON.stringify(data, null, 2); + log('Показатели очереди обновлены', { queue_id: data.queue_id, in_queue: data.in_queue }); + await Promise.all([loadRealtime(), loadKpi()]); + } catch (err) { + $('queueMetricsOutput').textContent = err.message; + log('Не удалось обновить очередь', { error: err.message }); + } +} + +function setRecordingPreviewEmpty(message) { + $('recordingMetaOutput').textContent = message || ''; + $('recordingSelectionHint').textContent = message || 'Выберите запись для предпросмотра.'; + $('recordingPlayer').removeAttribute('src'); + $('recordingPlayer').load(); + $('downloadRecordingBtn').disabled = true; + state.selectedRecordingId = null; + if (state.recordingPreviewUrl) { + URL.revokeObjectURL(state.recordingPreviewUrl); + state.recordingPreviewUrl = null; + } +} + +function renderRecordingsList(data) { + if (!data.length) { + $('recordingsList').innerHTML = '
          • Записи не найдены.
          • '; + setRecordingPreviewEmpty('По текущим фильтрам записи не найдены.'); + return; + } + + $('recordingsList').innerHTML = data + .map( + (item) => ` +
          • + ${item.recording_id}
            + звонок: ${item.call_id}
            + файл: ${item.file_name}
            + статус: ${item.status} +
            + +
            +
          • + `, + ) + .join(''); + + document.querySelectorAll('.recording-open-btn').forEach((button) => { + button.addEventListener('click', () => { + void openRecording(button.dataset.recordingId || ''); + }); + }); +} + +function renderRecordingMeta(data) { + $('recordingMetaOutput').textContent = JSON.stringify(data, null, 2); + $('recordingSelectionHint').textContent = `Выбрано: ${data.recording_id} | статус: ${data.status}`; + $('downloadRecordingBtn').disabled = false; +} + +async function loadRecordings() { + try { + const params = new URLSearchParams(); + const callId = $('recordingCallId').value.trim(); + const interactionId = $('recordingInteractionId').value.trim(); + const status = $('recordingStatus').value; + if (callId) { + params.set('call_id', callId); + } + if (interactionId) { + params.set('interaction_id', interactionId); + } + if (status) { + params.set('status', status); + } + params.set('limit', '50'); + + const suffix = params.toString() ? `?${params.toString()}` : ''; + const data = await api('recording', `recordings${suffix}`); + renderRecordingsList(data); + log('Список записей обновлён', { count: data.length }); + } catch (err) { + $('recordingsList').innerHTML = '
          • Не удалось загрузить записи.
          • '; + setRecordingPreviewEmpty(`Ошибка: ${err.message}`); + log('Не удалось загрузить записи', { error: err.message }); + } +} + +async function loadRecordingPreview(recordingId) { + const response = await fetchBinary('recording', `recordings/${encodeURIComponent(recordingId)}/content?disposition=inline`); + const blob = await response.blob(); + if (state.recordingPreviewUrl) { + URL.revokeObjectURL(state.recordingPreviewUrl); + } + state.recordingPreviewUrl = URL.createObjectURL(blob); + $('recordingPlayer').src = state.recordingPreviewUrl; + $('recordingPlayer').load(); +} + +async function openRecording(recordingId) { + if (!recordingId) { + return; + } + try { + const data = await api('recording', `recordings/${encodeURIComponent(recordingId)}`); + state.selectedRecordingId = recordingId; + renderRecordingMeta(data); + await loadRecordingPreview(recordingId); + log('Запись открыта', { recording_id: recordingId }); + } catch (err) { + setRecordingPreviewEmpty(`Не удалось открыть запись: ${err.message}`); + log('Не удалось открыть запись', { error: err.message }); + } +} + +async function resolveRecordingEvent(rawValue) { + const value = (rawValue || '').trim(); + if (!value) { + return null; + } + + const events = await api('voice', 'integrations/voice/events?limit=300'); + if (value.startsWith('vev_')) { + const direct = events.find((item) => item.event_id === value) || null; + return direct ? { eventId: value, event: direct } : { eventId: value, event: null }; + } + + const recordingEvents = events.filter((item) => item.event_type === 'recording.ready'); + for (let i = recordingEvents.length - 1; i >= 0; i -= 1) { + const item = recordingEvents[i]; + if (item.interaction_id === value || item.call_id === value) { + return { eventId: item.event_id, event: item }; + } + } + return null; +} + +async function resolveVoiceContext(rawValue) { + const value = (rawValue || '').trim(); + if (!value) { + return null; + } + const events = await api('voice', 'integrations/voice/events?limit=300'); + const matches = events.filter( + (item) => item.event_id === value || item.interaction_id === value || item.call_id === value, + ); + if (!matches.length) { + return null; + } + return matches[matches.length - 1]; +} + +async function importRecording() { + const rawInput = $('recordingEventId').value.trim(); + if (!rawInput) { + setRecordingPreviewEmpty('Укажите ID voice_event, обращения или звонка перед импортом.'); + return; + } + try { + const resolved = await resolveRecordingEvent(rawInput); + if (!resolved?.eventId) { + const ctx = await resolveVoiceContext(rawInput); + const callId = ctx?.call_id || ''; + const interactionId = ctx?.interaction_id || ''; + if (callId) { + $('recordingCallId').value = callId; + } + if (interactionId) { + $('recordingInteractionId').value = interactionId; + } + + if (callId) { + const existing = await api('recording', `recordings?call_id=${encodeURIComponent(callId)}&limit=10`); + if (Array.isArray(existing) && existing.length) { + await loadRecordings(); + await openRecording(existing[0].recording_id); + log('Запись уже импортирована автоматически', { call_id: callId, recording_id: existing[0].recording_id }); + return; + } + } + + const reason = ctx?.event_type === 'call.started' + ? 'Звонок ещё активен или только что завершился. Подождите 5-15 секунд до события recording.ready.' + : `Событие recording.ready для "${rawInput}" не найдено.`; + setRecordingPreviewEmpty(reason); + log('Импорт записи не выполнен', { error: reason, input: rawInput }); + return; + } + const { eventId, event } = resolved; + if (eventId !== rawInput) { + $('recordingEventId').value = eventId; + log('Событие записи найдено автоматически', { input: rawInput, event_id: eventId }); + } + + if (event?.payload?.source === 'asterisk' && !event?.payload?.source_path) { + $('recordingCallId').value = event.call_id || ''; + $('recordingInteractionId').value = event.interaction_id || ''; + await loadRecordings(); + setRecordingPreviewEmpty( + 'Записи Asterisk импортируются автоматически. Используйте список ниже и откройте последнюю запись.', + ); + log('Ручной импорт пропущен для события Asterisk', { event_id: eventId, call_id: event.call_id }); + return; + } + + const data = await api('recording', `recordings/import-from-voice-event/${encodeURIComponent(eventId)}`, { + method: 'POST', + }); + $('recordingCallId').value = data.call_id; + $('recordingInteractionId').value = data.interaction_id || ''; + await loadRecordings(); + await openRecording(data.recording_id); + log('Запись импортирована', { recording_id: data.recording_id, event_id: eventId }); + } catch (err) { + setRecordingPreviewEmpty(`Импорт не выполнен: ${err.message}`); + log('Импорт записи не выполнен', { error: err.message }); + } +} + +async function archiveRecording() { + if (!state.selectedRecordingId) { + setRecordingPreviewEmpty('Сначала откройте запись.'); + return; + } + try { + const data = await api('recording', `recordings/${encodeURIComponent(state.selectedRecordingId)}/archive`, { + method: 'POST', + }); + renderRecordingMeta(data); + await loadRecordings(); + log('Запись отправлена в архив', { recording_id: data.recording_id }); + } catch (err) { + log('Не удалось отправить запись в архив', { error: err.message }); + } +} + +async function downloadRecording() { + if (!state.selectedRecordingId) { + setRecordingPreviewEmpty('Сначала откройте запись.'); + return; + } + try { + const response = await fetchBinary( + 'recording', + `recordings/${encodeURIComponent(state.selectedRecordingId)}/content?disposition=attachment`, + ); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + const contentDisposition = response.headers.get('Content-Disposition') || ''; + const fileNameMatch = /filename=\"?([^\";]+)\"?/i.exec(contentDisposition); + link.href = url; + link.download = fileNameMatch?.[1] || `${state.selectedRecordingId}.wav`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + log('Запись скачана', { recording_id: state.selectedRecordingId }); + } catch (err) { + log('Не удалось скачать запись', { error: err.message }); + } +} + +function wire() { + $('loginBtn').addEventListener('click', login); + $('corporateLoginBtn').addEventListener('click', startCorporateLogin); + $('refreshBtn').addEventListener('click', async () => { + await Promise.all([loadRealtime(), loadKpi(), loadLiveCalls(), loadAgents(), loadRecordings(), refreshKnowledgeBase(true)]); + log('Данные супервизора обновлены'); + }); + $('loadRealtimeBtn').addEventListener('click', loadRealtime); + $('loadKpiBtn').addEventListener('click', loadKpi); + $('loadLiveCallsBtn').addEventListener('click', () => loadLiveCalls()); + $('loadAgentsBtn').addEventListener('click', loadAgents); + $('setAgentBtn').addEventListener('click', setAgentState); + $('saveQueueMetricsBtn').addEventListener('click', saveQueueMetrics); + $('createKbCategoryBtn').addEventListener('click', createKnowledgeCategory); + $('loadKbBtn').addEventListener('click', () => refreshKnowledgeBase()); + $('searchKbBtn').addEventListener('click', () => loadKnowledgeArticles()); + $('saveKbArticleBtn').addEventListener('click', saveKnowledgeArticle); + $('resetKbArticleBtn').addEventListener('click', resetKnowledgeArticleForm); + $('kbArticlesList').addEventListener('click', openKnowledgeArticleFromList); + $('importRecordingBtn').addEventListener('click', importRecording); + $('loadRecordingsBtn').addEventListener('click', loadRecordings); + $('archiveRecordingBtn').addEventListener('click', archiveRecording); + $('downloadRecordingBtn').addEventListener('click', downloadRecording); + $('logoutBtn').addEventListener('click', logout); + window.addEventListener('message', handleOidcMessage); +} + +async function init() { + if (!restoreStoredSession()) { + window.location.href = '/'; + return; + } + if (!ensurePageAccess()) { + return; + } + wire(); + updateSessionInfo(); + setRecordingPreviewEmpty(''); + resetKnowledgeArticleForm(); + await Promise.all([ + checkGateway(), + loadOidcConfig(), + loadRealtime(), + loadKpi(), + loadLiveCalls(true), + loadAgents(), + loadRecordings(), + refreshKnowledgeBase(true), + ]); + window.setInterval(() => { + void loadLiveCalls(true); + }, 5000); + log('Пульт супервизора готов'); +} + +init(); diff --git a/ui/supervisor/index.html b/ui/supervisor/index.html new file mode 100644 index 0000000..7531572 --- /dev/null +++ b/ui/supervisor/index.html @@ -0,0 +1,273 @@ + + + + + + KonturCC Супервизор + + + + + + + +
            + + +
            +
            + +
            +
            Шлюз: проверка...
            + +
            +
            SV
            +
            +
            Экран супервизора
            +
            Операционный контроль
            +
            +
            +
            +
            + +
            +
            +

            Операционный контур

            +

            Пульт супервизора

            +

            Операционный обзор: состояние команды, очередей и ключевых KPI.

            +
            + +
            +

            Сессия

            +
            + + +
            +
            + + +
            +
            + + +
            +
            + + + +
            +

            Токен: отсутствует

            +
            + +
            +
            +

            Оперативный срез

            +
            + + +
            +

            Этот блок нужен для оперативной проверки и технической диагностики на экране супервизора.

            +
            +
            + Технический ответ оперативного среза +
            
            +            
            +
            +
            + Технический ответ KPI +
            
            +            
            +
            + +
            +

            Живые звонки Asterisk

            +
            + +
            +

            Показываются последние звонки, пришедшие от Asterisk.

            +
              +
              + +
              +

              Агенты

              +
              + + +
              +
              + + +
              +
              + +
              +
                +
                + +
                +

                Очередь

                +
                + + +
                +
                + + +
                +

                Используйте это как быстрый операционный контроль для демонстрационного среза.

                +
                
                +          
                + +
                +

                База знаний

                +

                Супервизор может вручную создать категорию, добавить статью и сразу обновить базу знаний для Voice AI.

                +
                + +
                + + +
                +
                + + +
                + +
                + + +
                +
                + +
                +
                + +
                +
                + +
                +
                + + +
                +
                + + +
                +

                Создайте новую статью или выберите существующую для редактирования.

                + +
                + + +
                + +
                +
                +

                Категории

                +
                  +
                  +
                  +

                  Статьи

                  +
                    +
                    +
                    +
                    + Предпросмотр выбранной статьи +
                    
                    +            
                    +
                    + +
                    +

                    Лог действий

                    +

                    Сверху всегда последнее действие супервизора.

                    +
                    
                    +          
                    + +
                    +

                    Записи

                    +
                    + + +
                    +
                    + + +
                    +
                    + + + +
                    +

                    Для Asterisk запись обычно импортируется автоматически после завершения звонка.

                    +
                      +
                      +
                      Выберите запись для предпросмотра.
                      +
                      
                      +              
                      +              
                      +            
                      +
                      +
                      +
                      +
                      +
                      + + + +