246 lines
12 KiB
HTML
246 lines
12 KiB
HTML
{% extends "page.html" %}
|
|
{% block page_content %}
|
|
<script>var USER_ROLE = '{{ user.role }}';</script>
|
|
<style>
|
|
.consent-table { width:100%; border-collapse:collapse; }
|
|
.consent-table th, .consent-table td { padding:10px 14px; text-align:left; border-bottom:1px solid var(--border); font-size:13px; }
|
|
.consent-table th { font-weight:600; color:var(--text-muted); background:var(--bg-card); }
|
|
.consent-table td { vertical-align:middle; }
|
|
.consent-yes { color:var(--success, #155724); font-weight:600; }
|
|
.consent-no { color:var(--danger, #721c24); font-weight:600; }
|
|
.filter-bar { display:flex; gap:12px; margin-bottom:16px; align-items:center; flex-wrap:wrap; }
|
|
.filter-bar input, .filter-bar select { padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:13px; }
|
|
.broadcast-card { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:20px; margin-top:16px; }
|
|
.broadcast-card h3 { margin-bottom:12px; }
|
|
.broadcast-card textarea { width:100%; padding:8px 12px; border:1px solid var(--border); border-radius:6px; font-size:14px; min-height:100px; resize:vertical; margin-bottom:10px; box-sizing:border-box; }
|
|
.broadcast-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
|
.recipient-count { font-size:12px; color:var(--text-muted); margin-bottom:8px; }
|
|
|
|
.preview-area { display:none; margin-top:12px; padding:12px; border:1px solid var(--border); border-radius:8px; background:var(--bg-input); white-space:pre-wrap; font-size:14px; line-height:1.5; }
|
|
.preview-area.open { display:block; }
|
|
.preview-area .preview-text { margin-bottom:10px; }
|
|
.preview-area .preview-actions { display:flex; gap:10px; }
|
|
|
|
.confirm-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
|
|
.confirm-overlay.open { display:flex; }
|
|
.confirm-modal { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:24px; width:500px; max-width:90vw; }
|
|
.confirm-modal h3 { margin-bottom:8px; }
|
|
.confirm-modal .confirm-text { padding:10px; border:1px solid var(--border); border-radius:6px; background:var(--bg-input); max-height:200px; overflow-y:auto; margin:12px 0; white-space:pre-wrap; font-size:13px; }
|
|
.confirm-modal .confirm-count { font-size:12px; color:var(--text-muted); margin-bottom:12px; }
|
|
.confirm-modal .confirm-timer { font-size:24px; font-weight:700; text-align:center; color:var(--accent); margin:12px 0; }
|
|
.confirm-modal .confirm-actions { display:flex; gap:10px; justify-content:center; }
|
|
.confirm-modal .confirm-actions .btn { min-width:140px; }
|
|
.timer-danger { color:var(--danger) !important; }
|
|
</style>
|
|
|
|
<div class="card">
|
|
<div class="card-header" style="display:flex;justify-content:space-between;align-items:center;">
|
|
<h2>Согласия ФЗ-152</h2>
|
|
<span id="consent-count" style="font-size:13px;color:var(--text-muted);">0 пользователей</span>
|
|
</div>
|
|
|
|
<div class="filter-bar">
|
|
<input type="text" id="consent-search" placeholder="Поиск по имени, телефону..." oninput="filterConsents()">
|
|
</div>
|
|
|
|
<table class="consent-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Имя</th>
|
|
<th>Телефон</th>
|
|
<th>Email</th>
|
|
<th>Организация</th>
|
|
<th>Username</th>
|
|
<th>Согласие</th>
|
|
<th>Дата</th>
|
|
{% if user.role == 'owner' %}<th>Действия</th>{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody id="consent-table-body">
|
|
<tr><td colspan="{% if user.role == 'owner' %}9{% else %}8{% endif %}" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="broadcast-card">
|
|
<h3>Рассылка пользователям MAX</h3>
|
|
<div class="recipient-count" id="recipient-count">Получателей: 0</div>
|
|
<textarea id="broadcast-text" placeholder="Введите текст сообщения..." oninput="clearPreview()"></textarea>
|
|
<div class="broadcast-actions">
|
|
<button class="btn" onclick="showPreview()">Предпросмотр</button>
|
|
<button class="btn btn-primary" id="send-btn" onclick="openConfirm()" disabled>Отправить всем</button>
|
|
</div>
|
|
<div class="preview-area" id="preview-area">
|
|
<div class="preview-text" id="preview-text"></div>
|
|
<div class="preview-actions">
|
|
<button class="btn btn-primary" onclick="openConfirm()">Отправить</button>
|
|
<button class="btn" onclick="hidePreview()">Редактировать</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="confirm-overlay" id="confirm-overlay">
|
|
<div class="confirm-modal">
|
|
<h3>Подтверждение рассылки</h3>
|
|
<div class="confirm-count" id="confirm-count">Получателей: 0</div>
|
|
<div class="confirm-text" id="confirm-text"></div>
|
|
<div class="confirm-timer" id="confirm-timer">10</div>
|
|
<div class="confirm-actions">
|
|
<button class="btn btn-primary" id="confirm-yes" onclick="executeBroadcast()">Подтвердить</button>
|
|
<button class="btn" onclick="closeConfirm()">Отмена</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
var allConsents = [];
|
|
var confirmTimer = null;
|
|
var confirmSeconds = 10;
|
|
|
|
function loadConsents(){
|
|
var tbody = document.getElementById('consent-table-body');
|
|
var colspan = USER_ROLE === 'owner' ? 9 : 8;
|
|
tbody.innerHTML = '<tr><td colspan="' + colspan + '" style="text-align:center;padding:30px;color:var(--text-muted);">Загрузка...</td></tr>';
|
|
fetch('/api/bot/users/consented').then(function(r){ return r.json() }).then(function(data){
|
|
allConsents = data;
|
|
document.getElementById('consent-count').textContent = data.length + ' пользователей';
|
|
document.getElementById('recipient-count').textContent = 'Получателей: ' + data.length;
|
|
document.getElementById('send-btn').disabled = data.length === 0;
|
|
renderConsents(data);
|
|
}).catch(function(err){
|
|
tbody.innerHTML = '<tr><td colspan="' + colspan + '" style="text-align:center;padding:30px;color:var(--text-muted);">Ошибка загрузки</td></tr>';
|
|
showNotification('Ошибка загрузки согласий', 'error');
|
|
});
|
|
}
|
|
|
|
function renderConsents(list){
|
|
var tbody = document.getElementById('consent-table-body');
|
|
var colspan = USER_ROLE === 'owner' ? 9 : 8;
|
|
if(!list.length){
|
|
tbody.innerHTML = '<tr><td colspan="' + colspan + '" style="text-align:center;padding:30px;color:var(--text-muted);">Нет данных</td></tr>';
|
|
return;
|
|
}
|
|
var html = '';
|
|
list.forEach(function(u){
|
|
var name = [u.first_name, u.last_name].filter(Boolean).join(' ') || '—';
|
|
var dateStr = u.consent_date ? new Date(u.consent_date).toLocaleString() : '—';
|
|
html += '<tr>' +
|
|
'<td>' + u.id + '</td>' +
|
|
'<td>' + escapeHtml(name) + '</td>' +
|
|
'<td>' + escapeHtml(u.phone || '—') + '</td>' +
|
|
'<td>' + escapeHtml(u.email || '—') + '</td>' +
|
|
'<td>' + escapeHtml(u.organization || '—') + '</td>' +
|
|
'<td>' + escapeHtml(u.username || '—') + '</td>' +
|
|
'<td class="consent-yes">Да</td>' +
|
|
'<td style="font-size:12px;color:var(--text-muted);">' + dateStr + '</td>';
|
|
if(USER_ROLE === 'owner'){
|
|
html += '<td><button class="btn btn-danger btn-sm" onclick="confirmDeleteUser(' + u.id + ', \'' + escapeHtml(name).replace(/'/g, "\\'") + '\')" title="Удалить пользователя">🗑</button></td>';
|
|
}
|
|
html += '</tr>';
|
|
});
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
function filterConsents(){
|
|
var search = document.getElementById('consent-search').value.toLowerCase();
|
|
var filtered = allConsents.filter(function(u){
|
|
if(!search) return true;
|
|
var name = [u.first_name, u.last_name].filter(Boolean).join(' ').toLowerCase();
|
|
var phone = (u.phone || '').toLowerCase();
|
|
var email = (u.email || '').toLowerCase();
|
|
var username = (u.username || '').toLowerCase();
|
|
return name.includes(search) || phone.includes(search) || email.includes(search) || username.includes(search);
|
|
});
|
|
renderConsents(filtered);
|
|
}
|
|
|
|
function clearPreview(){
|
|
document.getElementById('preview-area').classList.remove('open');
|
|
}
|
|
|
|
function showPreview(){
|
|
var text = document.getElementById('broadcast-text').value;
|
|
if(!text.trim()){ showNotification('Введите текст сообщения', 'warning'); return; }
|
|
document.getElementById('preview-text').textContent = text;
|
|
document.getElementById('preview-area').classList.add('open');
|
|
}
|
|
|
|
function hidePreview(){
|
|
document.getElementById('preview-area').classList.remove('open');
|
|
}
|
|
|
|
function openConfirm(){
|
|
if(confirmTimer) return;
|
|
var text = document.getElementById('broadcast-text').value.trim();
|
|
if(!text){ showNotification('Введите текст сообщения', 'warning'); return; }
|
|
closeConfirm();
|
|
document.getElementById('confirm-text').textContent = text;
|
|
document.getElementById('confirm-count').textContent = 'Получателей: ' + allConsents.length;
|
|
document.getElementById('confirm-overlay').classList.add('open');
|
|
confirmSeconds = 10;
|
|
document.getElementById('confirm-timer').textContent = confirmSeconds;
|
|
document.getElementById('confirm-timer').className = 'confirm-timer';
|
|
document.getElementById('confirm-yes').disabled = false;
|
|
confirmTimer = setInterval(function(){
|
|
confirmSeconds--;
|
|
document.getElementById('confirm-timer').textContent = confirmSeconds;
|
|
if(confirmSeconds <= 3) document.getElementById('confirm-timer').className = 'confirm-timer timer-danger';
|
|
if(confirmSeconds <= 0){
|
|
clearInterval(confirmTimer);
|
|
confirmTimer = null;
|
|
closeConfirm();
|
|
showNotification('Время подтверждения истекло. Нажмите «Отправить всем» повторно.', 'warning');
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function closeConfirm(){
|
|
if(confirmTimer){ clearInterval(confirmTimer); confirmTimer = null; }
|
|
document.getElementById('confirm-overlay').classList.remove('open');
|
|
}
|
|
|
|
function executeBroadcast(){
|
|
if(confirmTimer){ clearInterval(confirmTimer); confirmTimer = null; }
|
|
var text = document.getElementById('confirm-text').textContent;
|
|
document.getElementById('confirm-yes').disabled = true;
|
|
fetch('/api/bot/broadcast', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({text: text, all: true})
|
|
}).then(function(r){ return r.json() }).then(function(d){
|
|
closeConfirm();
|
|
if(d.ok) showNotification('Отправлено: ' + d.sent + ', ошибок: ' + d.errors, d.errors ? 'warning' : 'success');
|
|
else showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
|
|
}).catch(function(){
|
|
closeConfirm();
|
|
showNotification('Ошибка соединения', 'error');
|
|
});
|
|
}
|
|
|
|
function escapeHtml(s){
|
|
if(!s) return '';
|
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
function confirmDeleteUser(userId, userName){
|
|
if(!confirm('Удалить пользователя ' + userName + ' (ID: ' + userId + ')?\n\nВсе его диалоги и сообщения будут удалены навсегда. Заявки останутся.')) return;
|
|
fetch('/api/bot/users/' + userId, { method: 'DELETE' })
|
|
.then(function(r){ return r.json() })
|
|
.then(function(d){
|
|
if(d.ok){
|
|
showNotification('Пользователь ' + userName + ' удалён', 'success');
|
|
allConsents = allConsents.filter(function(u){ return u.id !== userId; });
|
|
renderConsents(allConsents);
|
|
document.getElementById('consent-count').textContent = allConsents.length + ' пользователей';
|
|
document.getElementById('recipient-count').textContent = 'Получателей: ' + allConsents.length;
|
|
} else {
|
|
showNotification('Ошибка: ' + (d.error || 'неизвестная'), 'error');
|
|
}
|
|
}).catch(function(){
|
|
showNotification('Ошибка соединения', 'error');
|
|
});
|
|
}
|
|
|
|
loadConsents();
|
|
</script>
|
|
{% endblock %} |