domovoy_bot/web/templates/users.html
Admin 97c3e5864d 🎨 refactor: Единая навигация — ВСЕ 14 страниц используют base.html
БЫЛО: Каждая из 14 страниц имела своё собственное вшитое меню
      с разным набором кнопок (от 4 до 10 пунктов)

СТАЛО: Все 14 страниц наследуют base.html с единым меню из 14 пунктов
       в 4 разделах: Пользователи / Коммуникации / Контент / Система

Убрано 1949 строк дублирующегося кода, добавлено 1323 строки чистого контента.
Экономия: -626 строк (32% сокращение).

Переписанные страницы:
- dashboard.html, users.html, verification.html
- phones.html, polls.html, events.html
- schedules.html, ads.html, scheduled_posts.html, export.html

Результат: На ЛЮБОЙ странице пользователь видит ВСЕ 14 пунктов меню.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-12 18:52:58 +04:00

234 lines
16 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.

{% extends "base.html" %}
{% block title %}Пользователи - Домовой Бот{% endblock %}
{% block nav_users %}active{% endblock %}
{% block 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 -->
{% 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 %}
<!-- Search -->
<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>
</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 }})"><i class="bi bi-shield-x"></i></button>
{% else %}
<button class="btn btn-sm btn-success" onclick="verifyUser({{ user.user_id }})"><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 }})"><i class="bi bi-person-x"></i></button>
{% else %}
<button class="btn btn-sm btn-dark" onclick="makeAdmin({{ user.user_id }})"><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 }})"><i class="bi bi-person-dash"></i></button>
{% else %}
<button class="btn btn-sm btn-info" onclick="addToIg({{ user.user_id }})"><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>
<!-- Edit 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"></div>
<div class="mb-3"><label class="form-label">Телефон</label><input type="text" class="form-control" id="editPhone"></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>
{% endblock %}
{% block extra_script %}
<script>
async function quickVerify(userId, apartment) {
if (!confirm(`Верифицировать пользователя (кв. ${apartment})?`)) return;
const r = await fetch(`/api/user/${userId}/verify`, { method: 'POST' });
const result = await r.json();
if (result.success) { alert('✅ Пользователь верифицирован!'); location.reload(); }
}
async function quickReject(reqId) {
if (!confirm('Отклонить заявку на верификацию?')) return;
const r = await fetch(`/api/verification/${reqId}/reject`, { method: 'POST' });
const result = await r.json();
if (result.success) { alert('✅ Заявка отклонена'); location.reload(); }
}
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(); } }
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(); } }
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 r = await fetch('/api/users/auto_verify', { method: 'POST' }); const result = await r.json(); if (result.success) { alert('✅ ' + result.message); location.reload(); } }
</script>
{% endblock %}