264 lines
14 KiB
HTML
264 lines
14 KiB
HTML
{% extends "page.html" %}
|
||
{% block page_content %}
|
||
<div class="page-header">
|
||
<h1>Диалоги чат-бота</h1>
|
||
<div class="header-controls">
|
||
<input type="text" id="search-input" placeholder="Поиск по ID или имени..." oninput="filterConversations()" class="filter-input">
|
||
<button onclick="loadConversations()" class="btn btn-primary">Обновить</button>
|
||
</div>
|
||
</div>
|
||
<script>var USER_ROLE = '{{ user.role }}';</script>
|
||
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
|
||
<select id="status-filter" onchange="filterConversations()" class="filter-select">
|
||
<option value="">Все статусы</option>
|
||
<option value="greeting">Приветствие</option>
|
||
<option value="collecting_name">Сбор имени</option>
|
||
<option value="collecting_phone">Сбор телефона</option>
|
||
<option value="collecting_email">Сбор email</option>
|
||
<option value="ticket_creation">Создание заявки</option>
|
||
<option value="question">Вопрос</option>
|
||
<option value="done">Завершён</option>
|
||
</select>
|
||
</div>
|
||
<div id="conversations-container">
|
||
<div class="loading"><div class="spinner"></div></div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="dialog-modal">
|
||
<div class="modal" style="max-width:800px;width:90%;">
|
||
<button type="button" class="modal-close" onclick="closeDialog()">✕</button>
|
||
<h2 id="dialog-title">Диалог</h2>
|
||
<div style="display:flex;gap:4px;margin-bottom:12px;">
|
||
<button onclick="showTab('messages')" class="tab-btn active" id="tab-messages-btn">Сообщения</button>
|
||
<button onclick="showTab('logs')" class="tab-btn" id="tab-logs-btn">Логи обработки</button>
|
||
</div>
|
||
<div id="tab-messages" class="tab-content"></div>
|
||
<div id="tab-logs" class="tab-content" style="display:none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="delete-modal">
|
||
<div class="modal" style="max-width:420px;">
|
||
<button type="button" class="modal-close" onclick="closeDeleteModal()">✕</button>
|
||
<h2 style="font-size:16px;margin-bottom:8px;">Удалить диалог?</h2>
|
||
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
|
||
Диалог с <strong id="delete-conv-name"></strong> будет удалён навсегда. Это действие нельзя отменить.
|
||
</p>
|
||
<div class="modal-actions" style="justify-content:flex-end;">
|
||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Удалить</button>
|
||
<button type="button" class="btn" onclick="closeDeleteModal()">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.conv-card { background:var(--bg-card,#0A1628); border:1px solid var(--border,#253248); border-radius:8px; padding:12px; margin-bottom:8px; cursor:pointer; transition:border-color 0.2s; }
|
||
.conv-card:hover { border-color:var(--accent,#00ADEF); }
|
||
.conv-card .conv-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
|
||
.conv-card .conv-user { font-weight:600; color:var(--text-primary,#E8EDF2); }
|
||
.conv-card .conv-id { font-size:11px; color:var(--text-muted,#6B8299); }
|
||
.conv-card .conv-meta { display:flex; gap:12px; font-size:12px; color:var(--text-muted,#6B8299); }
|
||
.conv-card .conv-state { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; font-weight:600; }
|
||
.conv-state-greeting { background:#1a3a4a; color:#6bc4e8; }
|
||
.conv-state-collecting { background:#3a2a1a; color:#e8b86b; }
|
||
.conv-state-question { background:#1a3a2a; color:#6be8a0; }
|
||
.conv-state-done { background:#2a2a2a; color:#888; }
|
||
.conv-state-default { background:#2a1a3a; color:#b86be8; }
|
||
.msg-row { margin-bottom:8px; padding:8px 12px; border-radius:8px; max-width:80%; }
|
||
.msg-in { background:var(--bg-input,#1e3044); margin-right:auto; border-radius:8px 8px 8px 0; }
|
||
.msg-out { background:var(--accent,#00ADEF); margin-left:auto; border-radius:8px 8px 0 8px; color:#fff; }
|
||
.msg-meta { font-size:10px; color:var(--text-muted,#6B8299); margin-top:3px; }
|
||
.msg-out .msg-meta { color:rgba(255,255,255,0.7); }
|
||
.log-row { padding:6px 0; border-bottom:1px solid var(--border,#253248); font-size:12px; }
|
||
.log-step { font-weight:600; color:var(--accent,#00ADEF); }
|
||
.log-ok { color:#6be8a0; }
|
||
.log-error { color:#e86b6b; }
|
||
.log-time { font-size:10px; color:var(--text-muted,#6B8299); }
|
||
.tab-btn { padding:6px 14px; border:1px solid var(--border,#253248); background:transparent; color:var(--text-primary,#E8EDF2); border-radius:4px; cursor:pointer; font-size:12px; }
|
||
.tab-btn.active { background:var(--accent,#00ADEF); border-color:var(--accent,#00ADEF); color:#fff; }
|
||
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; flex-wrap:wrap; gap:8px; }
|
||
.header-controls { display:flex; gap:8px; align-items:center; }
|
||
.filter-input { padding:6px 10px; border:1px solid var(--border,#253248); border-radius:4px; background:var(--bg-body,#1B2838); color:var(--text-primary,#E8EDF2); font-size:12px; width:200px; }
|
||
.filter-select { padding:6px 10px; border:1px solid var(--border,#253248); border-radius:4px; background:var(--bg-body,#1B2838); color:var(--text-primary,#E8EDF2); font-size:12px; }
|
||
.conv-delete { background:none; border:none; cursor:pointer; font-size:14px; padding:2px 6px; border-radius:4px; opacity:0.5; transition:opacity 0.2s; margin-left:auto; }
|
||
.conv-delete:hover { opacity:1; background:rgba(232,107,107,0.15); }
|
||
.conv-header { display:flex; align-items:center; gap:8px; }
|
||
#delete-modal .modal { max-width:420px; }
|
||
#delete-modal h2 { font-size:16px; margin-bottom:8px; }
|
||
#delete-modal p { color:var(--text-secondary); font-size:14px; }
|
||
</style>
|
||
|
||
<script>
|
||
var conversations = [];
|
||
var currentConvId = null;
|
||
|
||
function loadConversations(){
|
||
var container = document.getElementById('conversations-container');
|
||
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||
fetch('/api/bot/conversations').then(function(r){ return r.json() }).then(function(data){
|
||
if(!Array.isArray(data)){
|
||
throw new Error(data && data.error ? data.error : 'Неверный формат ответа');
|
||
}
|
||
conversations = data;
|
||
renderConversations(data);
|
||
}).catch(function(err){
|
||
container.innerHTML = '<div class="error-message">Ошибка загрузки: ' + err.message + '</div>';
|
||
});
|
||
}
|
||
|
||
function renderConversations(data){
|
||
var container = document.getElementById('conversations-container');
|
||
if(!data || data.length === 0){
|
||
container.innerHTML = '<div class="empty-state">Нет диалогов</div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
var filter = document.getElementById('search-input').value.toLowerCase();
|
||
var statusFilter = document.getElementById('status-filter').value;
|
||
data.forEach(function(c){
|
||
var userName = c.user_name.toLowerCase();
|
||
var userId = String(c.user_id);
|
||
var state = c.state || '';
|
||
if(filter && !userName.includes(filter) && !userId.includes(filter)) return;
|
||
if(statusFilter && state !== statusFilter) return;
|
||
var stateClass = 'conv-state-default';
|
||
if(state === 'greeting') stateClass = 'conv-state-greeting';
|
||
else if(state.startsWith('collecting')) stateClass = 'conv-state-collecting';
|
||
else if(state === 'question') stateClass = 'conv-state-question';
|
||
else if(state === 'done') stateClass = 'conv-state-done';
|
||
var stateLabel = state || '—';
|
||
html += '<div class="conv-card" onclick="openDialog(' + c.id + ')">';
|
||
html += '<div class="conv-header"><span class="conv-user">' + escapeHtml(c.user_name) + '</span><span class="conv-id">ID: ' + c.id + '</span>';
|
||
if(USER_ROLE === 'owner'){
|
||
html += '<button class="conv-delete" onclick="event.stopPropagation();openDeleteModal(' + c.id + ', \'' + escapeHtml(c.user_name).replace(/'/g, "\\'") + '\')" title="Удалить диалог">✕</button>';
|
||
}
|
||
html += '</div>';
|
||
html += '<div class="conv-meta">';
|
||
html += '<span class="conv-state ' + stateClass + '">' + escapeHtml(stateLabel) + '</span>';
|
||
html += '<span>' + c.message_count + ' сообщ.</span>';
|
||
html += '<span>intent: ' + escapeHtml(c.intent || '—') + '</span>';
|
||
html += '<span>' + (c.created_at || '').substring(0, 16).replace('T', ' ') + '</span>';
|
||
html += '</div></div>';
|
||
});
|
||
container.innerHTML = html || '<div class="empty-state">Нет диалогов по фильтру</div>';
|
||
}
|
||
|
||
function filterConversations(){
|
||
renderConversations(conversations);
|
||
}
|
||
|
||
function openDialog(convId){
|
||
currentConvId = convId;
|
||
document.getElementById('dialog-modal').classList.add('open');
|
||
var conv = conversations.find(function(c){ return c.id === convId; });
|
||
document.getElementById('dialog-title').textContent = 'Диалог с ' + (conv ? conv.user_name : '#' + convId);
|
||
showTab('messages');
|
||
loadMessages(convId);
|
||
}
|
||
|
||
function closeDialog(){
|
||
document.getElementById('dialog-modal').classList.remove('open');
|
||
}
|
||
|
||
function showTab(tab){
|
||
document.querySelectorAll('.tab-content').forEach(function(el){ el.style.display = 'none'; });
|
||
document.querySelectorAll('.tab-btn').forEach(function(el){ el.classList.remove('active'); });
|
||
document.getElementById('tab-' + tab).style.display = 'block';
|
||
document.getElementById('tab-' + tab + '-btn').classList.add('active');
|
||
if(tab === 'logs' && currentConvId) loadLogs(currentConvId);
|
||
}
|
||
|
||
function loadMessages(convId){
|
||
var container = document.getElementById('tab-messages');
|
||
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||
fetch('/api/bot/conversations/' + convId + '/messages').then(function(r){ return r.json() }).then(function(data){
|
||
if(!Array.isArray(data)){
|
||
throw new Error(data && data.error ? data.error : 'Неверный формат ответа');
|
||
}
|
||
if(data.length === 0){
|
||
container.innerHTML = '<div class="empty-state">Нет сообщений</div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
data.forEach(function(m){
|
||
var dir = m.direction === 'incoming' ? 'msg-in' : 'msg-out';
|
||
var label = m.direction === 'incoming' ? 'Пользователь' : 'Бот';
|
||
html += '<div class="msg-row ' + dir + '">';
|
||
html += '<div>' + escapeHtml(m.text || '') + '</div>';
|
||
html += '<div class="msg-meta">' + label + ' · ' + (m.created_at || '').substring(0, 19).replace('T', ' ') + '</div>';
|
||
html += '</div>';
|
||
});
|
||
container.innerHTML = html;
|
||
}).catch(function(err){
|
||
container.innerHTML = '<div class="error-message">Ошибка: ' + err + '</div>';
|
||
});
|
||
}
|
||
|
||
function loadLogs(convId){
|
||
var container = document.getElementById('tab-logs');
|
||
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||
fetch('/api/bot/conversations/' + convId + '/logs').then(function(r){ return r.json() }).then(function(data){
|
||
if(!Array.isArray(data)){
|
||
throw new Error(data && data.error ? data.error : 'Неверный формат ответа');
|
||
}
|
||
if(data.length === 0){
|
||
container.innerHTML = '<div class="empty-state">Нет логов</div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
data.forEach(function(l){
|
||
var statusClass = l.status === 'error' ? 'log-error' : 'log-ok';
|
||
html += '<div class="log-row">';
|
||
html += '<span class="log-step">' + escapeHtml(l.step) + '</span> ';
|
||
html += '<span class="' + statusClass + '">[' + l.status + ']</span> ';
|
||
html += escapeHtml(l.message || '');
|
||
if(l.detail) html += '<div style="color:var(--text-muted,#6B8299);font-size:11px;margin-top:2px;">' + escapeHtml(l.detail) + '</div>';
|
||
html += '<div class="log-time">' + (l.created_at || '').substring(0, 19).replace('T', ' ') + '</div>';
|
||
html += '</div>';
|
||
});
|
||
container.innerHTML = html;
|
||
}).catch(function(err){
|
||
container.innerHTML = '<div class="error-message">Ошибка: ' + err + '</div>';
|
||
});
|
||
}
|
||
|
||
function escapeHtml(str){
|
||
if(!str) return '';
|
||
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
var deleteConvId = null;
|
||
|
||
function openDeleteModal(convId, convName){
|
||
deleteConvId = convId;
|
||
document.getElementById('delete-conv-name').textContent = convName || '#' + convId;
|
||
document.getElementById('delete-modal').classList.add('open');
|
||
}
|
||
|
||
function closeDeleteModal(){
|
||
document.getElementById('delete-modal').classList.remove('open');
|
||
deleteConvId = null;
|
||
}
|
||
|
||
function confirmDelete(){
|
||
if(!deleteConvId) return;
|
||
var id = deleteConvId;
|
||
closeDeleteModal();
|
||
fetch('/api/bot/conversations/' + id, { method: 'DELETE' })
|
||
.then(function(r){ return r.json() })
|
||
.then(function(d){
|
||
if(d.ok){
|
||
showNotification('Диалог #' + id + ' удалён', 'success');
|
||
conversations = conversations.filter(function(c){ return c.id !== id; });
|
||
renderConversations(conversations);
|
||
} else {
|
||
showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
|
||
}
|
||
}).catch(function(err){
|
||
showNotification('Ошибка соединения', 'error');
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', loadConversations);
|
||
</script>
|
||
{% endblock %} |