180 lines
8.0 KiB
HTML
180 lines
8.0 KiB
HTML
{% 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(--bg-card); }
|
||
.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:var(--info-bg, #cce5ff); color:var(--info, #004085); }
|
||
.status-v-rabote { background:var(--warning-bg, #fff3cd); color:var(--warning, #856404); }
|
||
.status-zakryta { background:var(--success-bg, #d4edda); color:var(--success, #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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
loadTickets();
|
||
</script>
|
||
{% endblock %}
|