Add operator unified messenger MVP
This commit is contained in:
+712
-5
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user