1419 lines
56 KiB
JavaScript
1419 lines
56 KiB
JavaScript
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 = '<li class="empty-state">Пользователи пока не найдены.</li>';
|
|
return;
|
|
}
|
|
$('usersList').innerHTML = items
|
|
.map((item) => `<li><strong>${item.username}</strong><br/>${item.full_name}<br/>роль: ${item.role}<br/>ID: ${item.user_id}</li>`)
|
|
.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 = '<li class="empty-state">Очереди пока не найдены.</li>';
|
|
$('routeQueueId').value = '';
|
|
$('updateQueueId').value = '';
|
|
$('ivrQueueId').value = '';
|
|
$('ivrTestQueueId').value = '';
|
|
return;
|
|
}
|
|
$('queuesList').innerHTML = items
|
|
.map((item) => `<li><strong>${item.name}</strong><br/>ID: ${item.queue_id}<br/>правил: ${(item.rules || []).length}</li>`)
|
|
.join('');
|
|
}
|
|
|
|
function renderIvrFlows(items) {
|
|
if (!items.length) {
|
|
$('ivrFlowsList').innerHTML = '<li class="empty-state">IVR-сценарии пока не настроены.</li>';
|
|
return;
|
|
}
|
|
$('ivrFlowsList').innerHTML = items
|
|
.map(
|
|
(item) =>
|
|
`<li><strong>${item.name}</strong><br/>ID: ${item.flow_id}<br/>очередь: ${item.queue_id}<br/>активен: ${item.is_active}</li>`,
|
|
)
|
|
.join('');
|
|
}
|
|
|
|
function renderAsteriskEvents(items) {
|
|
if (!items.length) {
|
|
$('asteriskEventsList').innerHTML = '<li class="empty-state">Bridge-события Asterisk не найдены.</li>';
|
|
return;
|
|
}
|
|
$('asteriskEventsList').innerHTML = items
|
|
.map(
|
|
(item) =>
|
|
`<li><strong>${item.ami_event_name}</strong><br/>событие: ${item.bridge_event_id}<br/>звонок: ${item.call_id}<br/>статус: ${item.forward_status}</li>`,
|
|
)
|
|
.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 = ['<option value="">Свой вариант</option>'].concat(
|
|
(options || []).map((item) => `<option value="${item.value}">${item.label}</option>`),
|
|
);
|
|
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 = '<li class="empty-state">Не удалось загрузить пользователей.</li>';
|
|
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 = '<li class="empty-state">Не удалось загрузить очереди.</li>';
|
|
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 = '<li class="empty-state">Не удалось загрузить IVR-сценарии.</li>';
|
|
$('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 = '<li class="empty-state">Не удалось загрузить bridge-события Asterisk.</li>';
|
|
$('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();
|