🎯 ИНИЦИАТИВНАЯ ГРУППА: • Кнопка 'ИГ' в дашборде и пользователях • Фильтр по ИГ в /users?filter=ig • Синяя карточка для ИГ ⚙️ НАСТРОЙКИ: • Перенесено из /settings в /broadcast • Лимит рассылки ИГ (секунды) • GET/POST /api/settings/ig_broadcast_limit 📊 ЛОГИРОВАНИЕ: • Таблица activity_log • Логирование рассылок • API /api/activity_log 🐛 ИСПРАВЛЕНИЯ: • Удалена /settings (Internal Server Error) • Исправлен web/app.py (повреждённый код) 📁 ФАЙЛЫ: • web/app.py: фильтр ИГ, логи, настройка • web/templates/dashboard.html: кнопка ИГ • web/templates/users.html: кнопка ИГ • web/templates/broadcast.html: настройка лимита • database: activity_log table Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
463 lines
24 KiB
HTML
463 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Пользователи - Домовой Бот</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
||
<style>
|
||
.sidebar {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%);
|
||
}
|
||
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; }
|
||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); }
|
||
.content { padding: 2rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container-fluid">
|
||
<div class="row">
|
||
<div class="col-md-2 sidebar p-0">
|
||
<div class="p-3 text-white text-center">
|
||
<h4><i class="bi bi-house-door"></i> Домовой Бот</h4>
|
||
</div>
|
||
<nav class="nav flex-column">
|
||
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
|
||
<a class="nav-link active" href="/users"><i class="bi bi-people"></i> Пользователи</a>
|
||
<a class="nav-link" href="/verification"><i class="bi bi-shield-check"></i> Верификация</a>
|
||
<a class="nav-link" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
|
||
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
|
||
<a class="nav-link" href="/settings"><i class="bi bi-gear"></i> Настройки</a>
|
||
</nav>
|
||
</div>
|
||
|
||
<div class="col-md-10 content">
|
||
<h2 class="mb-4">
|
||
Пользователи
|
||
{% if current_filter %}
|
||
<span class="badge bg-info ms-2">
|
||
{% if current_filter == 'verified' %}
|
||
✅ Верифицированные
|
||
{% elif current_filter == 'unverified' %}
|
||
⏳ Не верифицированы
|
||
{% elif current_filter == 'ig' %}
|
||
🔵 Инициативная группа
|
||
{% elif current_filter == 'active_unverified' %}
|
||
🔥 Активные (50+ сообщ.)
|
||
{% else %}
|
||
Все
|
||
{% endif %}
|
||
</span>
|
||
<a href="/users" class="btn btn-sm btn-outline-secondary ms-2">
|
||
<i class="bi bi-x"></i> Сбросить фильтр
|
||
</a>
|
||
{% endif %}
|
||
</h2>
|
||
|
||
<!-- Stats -->
|
||
{% if stats %}
|
||
<div class="row mb-4">
|
||
<div class="col-md-3">
|
||
<a href="/users" style="text-decoration: none;">
|
||
<div class="card bg-primary text-white">
|
||
<div class="card-body text-center">
|
||
<h3>{{ stats.total }}</h3>
|
||
<small>Всего пользователей</small>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<a href="/users?filter=verified" style="text-decoration: none;">
|
||
<div class="card bg-success text-white">
|
||
<div class="card-body text-center">
|
||
<h3>{{ stats.verified }}</h3>
|
||
<small>Верифицировано</small>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<a href="/users?filter=ig" style="text-decoration: none;">
|
||
<div class="card text-white" style="background: linear-gradient(135deg, #17a2b8, #117a8b);">
|
||
<div class="card-body text-center">
|
||
<h3>{{ stats.ig }}</h3>
|
||
<small>Инициативная группа</small>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<a href="/users?filter=active_unverified" style="text-decoration: none;">
|
||
<div class="card bg-info text-white">
|
||
<div class="card-body text-center">
|
||
<h3>{{ stats.active_unverified }}</h3>
|
||
<small>Активных (50+ сообщений)</small>
|
||
</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Auto Verify Button -->
|
||
{% if stats and stats.active_unverified > 0 %}
|
||
<div class="card mb-4 bg-light">
|
||
<div class="card-body">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-8">
|
||
<h5 class="mb-0">⚡ Автоверификация активных</h5>
|
||
<small class="text-muted">
|
||
Верифицировать {{ stats.active_unverified }} пользователей с 50+ сообщениями
|
||
</small>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
<button class="btn btn-success" onclick="autoVerify()">
|
||
<i class="bi bi-lightning-charge"></i> Верифицировать всех
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<form method="GET" action="/users" class="row g-3">
|
||
<div class="col-md-10">
|
||
<input type="text" class="form-control" name="search"
|
||
placeholder="Поиск по имени, квартире, нику..."
|
||
value="{{ search }}">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<button type="submit" class="btn btn-primary w-100">
|
||
<i class="bi bi-search"></i> Поиск
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Users Table -->
|
||
<div class="card">
|
||
<div class="card-header bg-light">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-8">
|
||
<h5 class="mb-0">📋 Пользователи</h5>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#legendCollapse">
|
||
ℹ️ Легенда
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="collapse" id="legendCollapse">
|
||
<div class="card-body bg-light">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<h6 class="mb-2">🔘 Кнопки действий:</h6>
|
||
<ul class="list-unstyled mb-0">
|
||
<li><span class="btn btn-sm btn-primary"><i class="bi bi-pencil"></i></span> — Редактировать профиль</li>
|
||
<li><span class="btn btn-sm btn-success"><i class="bi bi-shield-check"></i></span> — Верифицировать</li>
|
||
<li><span class="btn btn-sm btn-warning"><i class="bi bi-shield-x"></i></span> — Отозвать верификацию</li>
|
||
<li><span class="btn btn-sm btn-danger"><i class="bi bi-lock"></i></span> — Забанить</li>
|
||
<li><span class="btn btn-sm btn-success"><i class="bi bi-unlock"></i></span> — Разбанить</li>
|
||
</ul>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<h6 class="mb-2">👥 Управление доступом:</h6>
|
||
<ul class="list-unstyled mb-0">
|
||
<li><span class="btn btn-sm btn-dark"><i class="bi bi-person-check"></i></span> — Назначить админом</li>
|
||
<li><span class="btn btn-sm btn-outline-dark"><i class="bi bi-person-x"></i></span> — Снять админа</li>
|
||
<li><span class="btn btn-sm btn-info"><i class="bi bi-person-plus"></i></span> — Добавить в ИГ</li>
|
||
<li><span class="btn btn-sm btn-outline-info"><i class="bi bi-person-dash"></i></span> — Удалить из ИГ</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Имя</th>
|
||
<th>Ник</th>
|
||
<th>Квартира</th>
|
||
<th>Телефон</th>
|
||
<th>Верифицирован</th>
|
||
<th>Бан</th>
|
||
<th>Рейтинг</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for user in users %}
|
||
{% set is_in_ig = user.user_id in ig_user_ids %}
|
||
<tr>
|
||
<td>{{ user.user_id }}</td>
|
||
<td>{{ user.full_name or '—' }}</td>
|
||
<td>@{{ user.username or 'нет' }}</td>
|
||
<td>
|
||
<span id="apt-{{ user.user_id }}">{{ user.apartment or '—' }}</span>
|
||
</td>
|
||
<td>
|
||
<span id="phone-{{ user.user_id }}">{{ user.phone or '—' }}</span>
|
||
</td>
|
||
<td>
|
||
{% if user.verified %}
|
||
<span class="badge bg-success">✅ Да</span>
|
||
{% else %}
|
||
<span class="badge bg-danger">❌ Нет</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if user.banned %}
|
||
<span class="badge bg-danger">🚫 Да</span>
|
||
{% else %}
|
||
<span class="badge bg-success">✅ Нет</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ user.rating }}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-primary" onclick="editUser({{ user.user_id }})">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
{% if user.banned %}
|
||
<button class="btn btn-sm btn-success" onclick="unbanUser({{ user.user_id }})">
|
||
<i class="bi bi-unlock"></i>
|
||
</button>
|
||
{% else %}
|
||
<button class="btn btn-sm btn-danger" onclick="banUser({{ user.user_id }})">
|
||
<i class="bi bi-lock"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if user.verified %}
|
||
<button class="btn btn-sm btn-warning" onclick="unverifyUser({{ user.user_id }})" title="Отозвать верификацию">
|
||
<i class="bi bi-shield-x"></i>
|
||
</button>
|
||
{% else %}
|
||
<button class="btn btn-sm btn-success" onclick="verifyUser({{ user.user_id }})" title="Верифицировать">
|
||
<i class="bi bi-shield-check"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if user.is_admin %}
|
||
<button class="btn btn-sm btn-outline-dark" onclick="removeAdmin({{ user.user_id }})" title="Снять админа">
|
||
<i class="bi bi-person-x"></i>
|
||
</button>
|
||
{% else %}
|
||
<button class="btn btn-sm btn-dark" onclick="makeAdmin({{ user.user_id }})" title="Назначить админом">
|
||
<i class="bi bi-person-check"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if is_in_ig %}
|
||
<button class="btn btn-sm btn-outline-info" onclick="removeFromIg({{ user.user_id }})" title="Удалить из ИГ">
|
||
<i class="bi bi-person-dash"></i>
|
||
</button>
|
||
{% else %}
|
||
<button class="btn btn-sm btn-info" onclick="addToIg({{ user.user_id }})" title="Добавить в ИГ">
|
||
<i class="bi bi-person-plus"></i>
|
||
</button>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
|
||
{% if not users %}
|
||
<p class="text-center text-muted">Пользователи не найдены</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal для редактирования -->
|
||
<div class="modal fade" id="editUserModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Редактировать пользователя</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="editUserId">
|
||
<div class="mb-3">
|
||
<label class="form-label">Квартира</label>
|
||
<input type="text" class="form-control" id="editApartment" placeholder="45 или 12А">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Телефон</label>
|
||
<input type="text" class="form-control" id="editPhone" placeholder="+7 XXX XXX-XX-XX">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Рейтинг</label>
|
||
<input type="number" class="form-control" id="editRating" value="0">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveUser()">Сохранить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
const editModal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
||
let currentUserId = null;
|
||
|
||
function editUser(userId) {
|
||
currentUserId = userId;
|
||
// Загружаем данные пользователя
|
||
fetch(`/api/user/${userId}`)
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const user = data.user;
|
||
document.getElementById('editUserId').value = user.id;
|
||
document.getElementById('editApartment').value = user.apartment || '';
|
||
document.getElementById('editPhone').value = user.phone || '';
|
||
document.getElementById('editRating').value = user.rating || 0;
|
||
editModal.show();
|
||
});
|
||
}
|
||
|
||
async function saveUser() {
|
||
const userId = document.getElementById('editUserId').value;
|
||
const data = {
|
||
apartment: document.getElementById('editApartment').value,
|
||
phone: document.getElementById('editPhone').value,
|
||
rating: parseInt(document.getElementById('editRating').value)
|
||
};
|
||
|
||
const response = await fetch(`/api/user/${userId}/update`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
alert('✅ Данные обновлены');
|
||
location.reload();
|
||
} else {
|
||
alert('❌ Ошибка: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function banUser(userId) {
|
||
if (!confirm('Забанить пользователя?')) return;
|
||
const r = await fetch(`/api/user/${userId}/ban`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('Пользователь забанен');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function unbanUser(userId) {
|
||
if (!confirm('Разбанить пользователя?')) return;
|
||
const r = await fetch(`/api/user/${userId}/unban`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('Пользователь разбанен');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function verifyUser(userId) {
|
||
if (!confirm('Верифицировать пользователя?')) return;
|
||
const r = await fetch(`/api/user/${userId}/verify`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('Пользователь верифицирован');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function unverifyUser(userId) {
|
||
if (!confirm('Отозвать верификацию?')) return;
|
||
const r = await fetch(`/api/user/${userId}/unverify`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('Верификация отозвана');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function makeAdmin(userId) {
|
||
if (!confirm('Назначить пользователем админом?')) return;
|
||
const r = await fetch(`/api/user/${userId}/make_admin`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('✅ Пользователь теперь админ!');
|
||
location.reload();
|
||
} else {
|
||
alert('❌ Ошибка: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function removeAdmin(userId) {
|
||
if (!confirm('Снять админа?')) return;
|
||
const r = await fetch(`/api/user/${userId}/remove_admin`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('✅ Админ снят');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function addToIg(userId) {
|
||
if (!confirm('Добавить в Инициативную Группу?')) return;
|
||
const r = await fetch(`/api/user/${userId}/add_to_ig`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('✅ Добавлен в ИГ!');
|
||
location.reload();
|
||
} else {
|
||
alert('❌ Ошибка: ' + result.error);
|
||
}
|
||
}
|
||
|
||
async function removeFromIg(userId) {
|
||
if (!confirm('Удалить из Инициативной Группы?')) return;
|
||
const r = await fetch(`/api/user/${userId}/remove_from_ig`, { method: 'POST' });
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
alert('✅ Удалён из ИГ');
|
||
location.reload();
|
||
}
|
||
}
|
||
|
||
async function autoVerify() {
|
||
if (!confirm('Верифицировать всех активных пользователей (50+ сообщений)?')) return;
|
||
|
||
const resultDiv = document.getElementById('autoVerifyResult');
|
||
if (resultDiv) {
|
||
resultDiv.innerHTML = '<div class="alert alert-info">Автоверификация...</div>';
|
||
} else {
|
||
alert('Автоверификация...');
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/users/auto_verify', { method: 'POST' });
|
||
const result = await r.json();
|
||
|
||
if (result.success) {
|
||
alert('✅ ' + result.message);
|
||
location.reload();
|
||
} else {
|
||
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
|
||
}
|
||
} catch (err) {
|
||
alert('❌ Ошибка: ' + err.message);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|