БЫЛО: Каждая из 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>
234 lines
16 KiB
HTML
234 lines
16 KiB
HTML
{% 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 %}
|