Add operator unified messenger MVP

This commit is contained in:
Yera All
2026-04-16 22:16:57 +05:00
parent 1206ff91ac
commit 3b5a7ad2e7
4 changed files with 1225 additions and 12 deletions
+712 -5
View File
@@ -62,6 +62,17 @@ const state = {
pendingAction: '',
pollTimer: null,
},
messenger: {
conversations: [],
selectedConversationId: '',
messages: [],
selectedSummary: null,
activeFilter: 'all',
composerText: '',
backendError: '',
pendingAction: '',
pollTimer: null,
},
whatsapp: {
chats: [],
selectedChatId: '',
@@ -377,6 +388,13 @@ const UNIFIED_INBOX_COLUMNS = [
{ key: 'calls', title: 'Активные звонки', emptyMessage: 'Активных звонков сейчас нет.' },
];
const MESSENGER_FILTERS = {
all: 'Все',
new: 'Новые',
mine: 'Мои',
ai: 'AI handoff',
};
const LIVE_TELEPHONY_LABELS = {
ringing: 'Звонит',
claimed: 'Взято',
@@ -400,6 +418,7 @@ const VOICE_AI_STATE_META = {
const OPERATOR_VIEW_IDS = {
workspace: 'workspaceView',
messages: 'messagesView',
customers: 'customersView',
'customer-profile': 'customerProfileView',
telegram: 'telegramView',
@@ -553,6 +572,7 @@ function logout() {
window.clearInterval(state.telegram.pollTimer);
state.telegram.pollTimer = null;
}
stopMessengerPolling();
stopWhatsappPolling();
clearStoredSession();
window.location.href = '/';
@@ -2412,7 +2432,9 @@ function applyTelegramCollections(threads, options = {}) {
if (!state.telegram.selectedThreadId) {
state.telegram.selectedThreadSummary = null;
}
syncMessengerConversations({ preserveSelection: true });
renderTelegramWorkspace();
renderMessengerWorkspace();
renderCustomerSpotlight();
renderUnifiedInbox();
}
@@ -2652,10 +2674,628 @@ function startTelegramPolling() {
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 = `
<div class="empty-state messenger-empty-state">
<strong>Backend недоступен</strong>
<span>${escapeHtml(state.messenger.backendError)}</span>
</div>
`;
return;
}
const conversations = messengerVisibleConversations();
if (!state.messenger.conversations.length) {
target.innerHTML = '<div class="empty-state messenger-empty-state">Новых сообщений пока нет</div>';
return;
}
if (!conversations.length) {
target.innerHTML = `<div class="empty-state messenger-empty-state">Нет диалогов в фильтре «${escapeHtml(MESSENGER_FILTERS[state.messenger.activeFilter] || 'Все')}»</div>`;
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 `
<button class="messenger-conversation-card${selected}" data-messenger-conversation-id="${escapeHtml(conversation.conversation_id)}" type="button">
<div class="telegram-thread-avatar${selected ? ' selected' : ''}" ${telegramAvatarStyle(conversation.display_name)}>${escapeHtml(customerInitials(conversation.display_name))}</div>
<div class="messenger-conversation-body">
<div class="messenger-conversation-head">
<span class="messenger-conversation-name">${escapeHtml(conversation.display_name)}</span>
<span class="messenger-conversation-time">${escapeHtml(telegramThreadTimeLabel(conversation.last_message_at))}</span>
</div>
<div class="messenger-conversation-preview-row">
<span class="messenger-conversation-preview">${escapeHtml(preview)}</span>
${conversation.unread > 0 && !selected ? `<span class="telegram-thread-unread">${conversation.unread}</span>` : ''}
</div>
<div class="messenger-conversation-badges">
<span class="messenger-channel-badge compact">Telegram</span>
<span class="messenger-status-badge">${escapeHtml(statusMeta(conversation.status).label)}</span>
${conversation.claimed_by_user ? `<span class="messenger-status-badge owner">${escapeHtml(conversation.claimed_by_user)}</span>` : ''}
${aiMeta ? `<span class="telegram-thread-ai-chip ${aiMeta.tone}">${escapeHtml(aiMeta.label)}</span>` : ''}
</div>
</div>
</button>
`;
})
.join('');
}
function renderMessengerMessages() {
const target = $('messengerMessagesTimeline');
const conversation = selectedMessengerConversation();
if (!target) {
return;
}
if (!conversation) {
target.innerHTML = '<div class="empty-state">Выберите диалог слева</div>';
return;
}
if (!state.messenger.messages.length) {
target.innerHTML = '<div class="empty-state">Сообщений пока нет.</div>';
return;
}
target.innerHTML = state.messenger.messages
.map((message) => {
const direction = message.direction || 'system';
if (direction === 'system') {
return `
<article class="telegram-message system">
<div class="telegram-message-system-pill">${escapeHtml(message.text || 'Системное сообщение')}</div>
</article>
`;
}
const mine = direction === 'outbound';
const aiBadge = message.author_type === 'ai'
? '<span class="telegram-message-badge ai">AI</span>'
: '';
return `
<article class="telegram-message ${mine ? 'me' : 'other'}">
<div class="telegram-message-bubble ${mine ? 'me' : 'other'}${message.author_type === 'ai' ? ' ai' : ''}">
${aiBadge}
<div class="telegram-message-body">${escapeHtml(message.text || 'Сообщение без текста.')}</div>
<div class="telegram-message-meta">
<span class="telegram-message-time">${escapeHtml(telegramThreadTimeLabel(message.created_at))}</span>
${mine ? telegramMessageStatusMarkup(message) : ''}
</div>
</div>
</article>
`;
})
.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;
}
@@ -3422,6 +4062,9 @@ function operatorHashState(hash = window.location.hash) {
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: '' };
}
@@ -3470,6 +4113,16 @@ function applyOperatorViewFromHash(options = {}) {
$(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(() => {
@@ -4129,8 +4782,8 @@ async function openUnifiedInboxItem(button) {
const customerId = button.dataset.customerId || '';
if (kind === 'telegram') {
window.location.hash = '#telegram-page';
await selectTelegramThread(threadId);
window.location.hash = '#messages';
await selectMessengerConversation(messengerConversationId('telegram', threadId));
return;
}
if (kind === 'call') {
@@ -6395,9 +7048,9 @@ async function handleCustomerActionClick(event) {
if (!threadId) {
return;
}
window.location.hash = '#telegram-page';
await selectTelegramThread(threadId);
log('Открыт Telegram клиента', { customer_id: customer.customer_id, thread_id: threadId });
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') {
@@ -6455,6 +7108,9 @@ function wire() {
$('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 });
}
@@ -6505,6 +7161,56 @@ function wire() {
$('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 || '';
@@ -6723,6 +7429,7 @@ async function init() {
updateSessionInfo();
syncLiveCallActionButtons();
renderUnifiedInbox();
renderMessengerWorkspace();
if (featureEnabled('whatsapp')) {
renderWhatsappWorkspace();
}