Files
site_aegisone/py_service/app/templates/pages/bot_tickets.html
T
angel 72b6879f4b v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure
- Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets)
- Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT)
- Q1: Broadcast preview modal with 10s confirmation timer
- Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary)
- Q4: bot_settings showNotification(), editable max_bot_id
- Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret
- Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists()
- Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py
- Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings
- Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py
- Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
2026-05-29 02:30:30 +03:00

180 lines
7.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "page.html" %}
{% block page_content %}
<style>
.tickets-table { width:100%; border-collapse:collapse; }
.tickets-table th, .tickets-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
.tickets-table th { font-weight:600; color:var(--text-muted); background:var(--light-bg); }
.tickets-table td { vertical-align:middle; }
.status-badge { display:inline-block; padding:3px 10px; border-radius:12px; font-size:12px; font-weight:600; }
.status-nova { background:#cce5ff; color:#004085; }
.status-v-rabote { background:#fff3cd; color:#856404; }
.status-zakryta { background:#d4edda; color:#155724; }
.status-select { padding:4px 8px; border-radius:6px; border:1px solid var(--border); font-size:12px; }
.ticket-detail-card { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:20px; margin-top:16px; display:none; }
.ticket-detail-card.open { display:block; }
.ticket-detail-card h3 { margin-bottom:12px; }
.ticket-detail-card .field { margin-bottom:8px; }
.ticket-detail-card .field label { font-weight:600; font-size:12px; color:var(--text-muted); display:block; }
.ticket-detail-card .field .value { font-size:14px; }
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
.filter-bar select, .filter-bar input { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
</style>
<div class="card">
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
<h2>Заявки от чат-бота</h2>
<span id="ticket-count" style="font-size:13px;color:var(--text-muted);">0 заявок</span>
</div>
<div class="filter-bar">
<select id="status-filter" onchange="filterTickets()">
<option value="">Все статусы</option>
<option value="Новая">Новая</option>
<option value="В работе">В работе</option>
<option value="Закрыта">Закрыта</option>
</select>
<input type="text" id="ticket-search" placeholder="Поиск..." oninput="filterTickets()">
<button class="btn btn-sm" onclick="loadTickets()">Обновить</button>
</div>
<table class="tickets-table">
<thead>
<tr>
<th>ID</th>
<th>Заголовок</th>
<th>Приоритет</th>
<th>Статус</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="tickets-body">
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
</tbody>
</table>
</div>
<div class="ticket-detail-card" id="ticket-detail">
<h3 id="ticket-detail-title">Заявка #</h3>
<div class="field"><label>Описание</label><div class="value" id="ticket-detail-desc"></div></div>
<div class="field"><label>Приоритет</label><div class="value" id="ticket-detail-priority"></div></div>
<div class="field"><label>Статус</label>
<select id="ticket-detail-status" onchange="updateTicketStatus()">
<option value="Новая">Новая</option>
<option value="В работе">В работе</option>
<option value="Закрыта">Закрыта</option>
</select>
</div>
<div class="field"><label>Дата</label><div class="value" id="ticket-detail-date"></div></div>
<button class="btn" onclick="closeDetail()">Закрыть</button>
</div>
<script>
var allTickets = [];
var currentTicketId = null;
function loadTickets(){
var tbody = document.getElementById('tickets-body');
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>';
fetch('/api/bot/tickets').then(function(r){ return r.json() }).then(function(data){
allTickets = data;
document.getElementById('ticket-count').textContent = data.length + ' заявок';
renderTickets(data);
});
}
function renderTickets(tickets){
var tbody = document.getElementById('tickets-body');
if(!tickets.length){
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-muted);">Нет заявок</td></tr>';
return;
}
var html = '';
tickets.forEach(function(t){
var statusClass = 'status-nova';
if(t.status === 'В работе') statusClass = 'status-v-rabote';
else if(t.status === 'Закрыта') statusClass = 'status-zakryta';
var dateStr = t.created_at ? new Date(t.created_at).toLocaleString() : '';
html += '<tr>' +
'<td>' + t.id + '</td>' +
'<td><a href="javascript:void(0)" onclick="showDetail(' + t.id + ')">' + escapeHtml(t.title) + '</a></td>' +
'<td>' + t.priority + '</td>' +
'<td><span class="status-badge ' + statusClass + '">' + t.status + '</span></td>' +
'<td style="font-size:12px;color:var(--text-muted);">' + dateStr + '</td>' +
'<td>' +
' <select class="status-select" onchange="changeStatus(' + t.id + ', this.value)">' +
' <option value="">Сменить статус</option>' +
' <option value="Новая">Новая</option>' +
' <option value="В работе">В работе</option>' +
' <option value="Закрыта">Закрыта</option>' +
' </select>' +
'</td>' +
'</tr>';
});
tbody.innerHTML = html;
}
function filterTickets(){
var statusFilter = document.getElementById('status-filter').value;
var search = document.getElementById('ticket-search').value.toLowerCase();
var filtered = allTickets.filter(function(t){
if(statusFilter && t.status !== statusFilter) return false;
if(search && !t.title.toLowerCase().includes(search) && !(t.description || '').toLowerCase().includes(search)) return false;
return true;
});
renderTickets(filtered);
}
function showDetail(id){
currentTicketId = id;
var ticket = allTickets.find(function(t){ return t.id === id; });
if(!ticket) return;
document.getElementById('ticket-detail-title').textContent = 'Заявка #' + ticket.id;
document.getElementById('ticket-detail-desc').textContent = ticket.description || '—';
document.getElementById('ticket-detail-priority').textContent = ticket.priority;
document.getElementById('ticket-detail-status').value = ticket.status;
document.getElementById('ticket-detail-date').textContent = ticket.created_at ? new Date(ticket.created_at).toLocaleString() : '—';
document.getElementById('ticket-detail').classList.add('open');
}
function closeDetail(){
document.getElementById('ticket-detail').classList.remove('open');
currentTicketId = null;
}
function changeStatus(id, status){
if(!status) return;
updateTicketStatusApi(id, status);
}
function updateTicketStatus(){
if(currentTicketId){
var status = document.getElementById('ticket-detail-status').value;
updateTicketStatusApi(currentTicketId, status);
}
}
function updateTicketStatusApi(id, status){
fetch('/api/bot/tickets/' + id, {
method: 'PATCH',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({status: status})
}).then(function(r){ return r.json() }).then(function(d){
if(d.ok){
loadTickets();
if(currentTicketId === id) showDetail(id);
} else {
showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
}
});
}
function escapeHtml(s){
if(!s) return '';
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
loadTickets();
</script>
{% endblock %}