domovoy_bot/web/templates/users.html
Admin 377b061ac9 v1.24 Уведомления о верификации
🔔 УВЕДОМЛЕНИЯ:
• Бейдж в меню: Верификация (N)
• Блок в Пользователях: Ожидают (N)
• Быстрая верификация: [] []
• Telegram уведомление админу

 БЫСТРАЯ ВЕРИФИКАЦИЯ:
• Прямо в разделе Пользователи
• Кнопки: Верифицировать / Отклонить
• Перезагрузка страницы

📁 ФАЙЛЫ:
• web/app.py: pending_count
• web/templates/users.html: блок уведомлений
• web/templates/dashboard.html: бейдж
• handlers/verification.py: уведомление в Telegram

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-11 18:51:57 +00:00

539 lines
28 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

<!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>
</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>
<!-- Pending Verifications Alert -->
{% if pending_count and pending_count > 0 %}
<div class="card mb-4 border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">
<i class="bi bi-bell"></i> Ожидают верификации: {{ pending_count }}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Пользователь</th>
<th>Квартира</th>
<th>Дата заявки</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for req in pending_requests %}
<tr>
<td>
<strong>{{ req.user_name or 'user' ~ req.user_id }}</strong><br>
<small class="text-muted">@{{ req.username or 'нет' }}</small>
</td>
<td><code>{{ req.apartment }}</code></td>
<td>{{ req.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
<td>
<button class="btn btn-sm btn-success" onclick="quickVerify({{ req.user_id }}, '{{ req.apartment }}')" title="Верифицировать">
<i class="bi bi-check-lg"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="quickReject({{ req.id }})" title="Отклонить">
<i class="bi bi-x-lg"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- 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>
// Быстрая верификация
async function quickVerify(userId, apartment) {
if (!confirm(`Верифицировать пользователя (кв. ${apartment})?`)) return;
try {
const r = await fetch(`/api/user/${userId}/verify`, { method: 'POST' });
const result = await r.json();
if (result.success) {
alert('✅ Пользователь верифицирован!');
location.reload();
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
}
}
// Быстрое отклонение
async function quickReject(reqId) {
if (!confirm('Отклонить заявку на верификацию?')) return;
try {
const r = await fetch(`/api/verification/${reqId}/reject`, { method: 'POST' });
const result = await r.json();
if (result.success) {
alert('✅ Заявка отклонена');
location.reload();
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
}
}
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>