🎨 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>
This commit is contained in:
Admin 2026-04-12 18:52:58 +04:00
parent bd3a56ab3d
commit 97c3e5864d
10 changed files with 1334 additions and 1960 deletions

View file

@ -1,122 +1,93 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head>
<meta charset="UTF-8">
<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" 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 active" href="/ads"><i class="bi bi-megaphone"></i> Объявления</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></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"><i class="bi bi-megaphone"></i> Объявления</h2>
<div class="card mb-4"> {% block title %}Объявления - Домовой Бот{% endblock %}
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить объявление</h5>
</div>
<div class="card-body">
<form id="addAdForm" class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" id="adTitle" placeholder="Заголовок" required>
</div>
<div class="col-md-3">
<select class="form-select" id="adCategory">
<option value="sell">💰 Продам</option>
<option value="buy">🛒 Куплю</option>
<option value="give">🎁 Отдам</option>
<option value="lost">😿 Потеряно</option>
<option value="found">😺 Нашлось</option>
<option value="service">🔧 Услуга</option>
</select>
</div>
<div class="col-md-5">
<textarea class="form-control" id="adText" placeholder="Текст объявления" rows="1" required></textarea>
</div>
<div class="col-md-12">
<button type="submit" class="btn btn-primary">Добавить объявление</button>
</div>
</form>
</div>
</div>
<div class="card"> {% block nav_ads %}active{% endblock %}
<div class="card-body">
<table class="table table-hover"> {% block content %}
<thead> <h2 class="mb-4"><i class="bi bi-megaphone"></i> Объявления</h2>
<tr><th>Категория</th><th>Заголовок</th><th>Текст</th><th>Дата</th><th>Статус</th><th>Действия</th></tr>
</thead> <div class="card mb-4">
<tbody> <div class="card-header bg-primary text-white">
{% for ad in ads %} <h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить объявление</h5>
<tr>
<td>{{ ad.category }}</td>
<td>{{ ad.title }}</td>
<td>{{ ad.description[:100] if ad.description else '...' }}</td>
<td>{{ ad.created_at.strftime('%d.%m.%Y') if ad.created_at else '—' }}</td>
<td>
{% if ad.is_active %}
<span class="badge bg-success">✅ Активно</span>
{% else %}
<span class="badge bg-secondary">❌ Архив</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteAd({{ ad.ad_id }})">
<i class="bi bi-trash"></i> Удалить
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="card-body">
<script> <form id="addAdForm" class="row g-3">
document.getElementById('addAdForm').addEventListener('submit', async (e) => { <div class="col-md-4">
e.preventDefault(); <input type="text" class="form-control" id="adTitle" placeholder="Заголовок" required>
const data = { </div>
title: document.getElementById('adTitle').value, <div class="col-md-3">
text: document.getElementById('adText').value, <select class="form-select" id="adCategory">
category: document.getElementById('adCategory').value, <option value="sell">💰 Продам</option>
is_active: true <option value="buy">🛒 Куплю</option>
}; <option value="give">🎁 Отдам</option>
const r = await fetch('/api/ad/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }); <option value="lost">😿 Потеряно</option>
const result = await r.json(); <option value="found">😺 Нашлось</option>
if (result.success) { alert('✅ Объявление добавлено'); location.reload(); } <option value="service">🔧 Услуга</option>
}); </select>
async function deleteAd(id) { </div>
if (!confirm('Удалить объявление?')) return; <div class="col-md-5">
const r = await fetch(`/api/ad/${id}/delete`, { method: 'POST' }); <textarea class="form-control" id="adText" placeholder="Текст объявления" rows="1" required></textarea>
const result = await r.json(); </div>
if (result.success) { alert('✅ Удалено'); location.reload(); } <div class="col-md-12">
} <button type="submit" class="btn btn-primary">Добавить объявление</button>
</script> </div>
</body> </form>
</html> </div>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr><th>Категория</th><th>Заголовок</th><th>Текст</th><th>Дата</th><th>Статус</th><th>Действия</th></tr>
</thead>
<tbody>
{% for ad in ads %}
<tr>
<td>{{ ad.category }}</td>
<td>{{ ad.title }}</td>
<td>{{ ad.description[:100] if ad.description else '...' }}</td>
<td>{{ ad.created_at.strftime('%d.%m.%Y') if ad.created_at else '—' }}</td>
<td>
{% if ad.is_active %}
<span class="badge bg-success">✅ Активно</span>
{% else %}
<span class="badge bg-secondary">❌ Архив</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteAd({{ ad.ad_id }})">
<i class="bi bi-trash"></i> Удалить
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('addAdForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
title: document.getElementById('adTitle').value,
text: document.getElementById('adText').value,
category: document.getElementById('adCategory').value,
is_active: true
};
const r = await fetch('/api/ad/add', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) });
const result = await r.json();
if (result.success) { alert('✅ Объявление добавлено'); location.reload(); }
});
async function deleteAd(id) {
if (!confirm('Удалить объявление?')) return;
const r = await fetch(`/api/ad/${id}/delete`, { method: 'POST' });
const result = await r.json();
if (result.success) { alert('✅ Удалено'); location.reload(); }
}
</script>
{% endblock %}

View file

@ -1,211 +1,119 @@
<!DOCTYPE html> {% extends "base.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>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--success-color: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
}
body { {% block title %}Дашборд - Домовой Бот{% endblock %}
background-color: #f8f9fa;
}
.sidebar { {% block nav_dashboard %}active{% endblock %}
min-height: 100vh;
background: linear-gradient(180deg, var(--primary-color) 0%, #1a252f 100%);
}
.sidebar .nav-link { {% block extra_style %}
color: rgba(255,255,255,0.8); <style>
padding: 1rem; .stat-card {
transition: all 0.3s; border-radius: 10px;
} padding: 1.5rem;
color: white;
margin-bottom: 1rem;
transition: transform 0.3s;
}
.stat-card:hover { transform: translateY(-5px); }
.stat-card.primary { background: linear-gradient(135deg, #3498db, #2980b9); }
.stat-card.success { background: linear-gradient(135deg, #27ae60, #229954); }
.stat-card.info { background: linear-gradient(135deg, #17a2b8, #117a8b); }
.stat-card.danger { background: linear-gradient(135deg, #e74c3c, #c0392b); }
.stat-card .stat-value { font-size: 2.5rem; font-weight: bold; }
.stat-card .stat-label { font-size: 0.9rem; opacity: 0.9; }
</style>
{% endblock %}
.sidebar .nav-link:hover, {% block content %}
.sidebar .nav-link.active { <div class="d-flex justify-content-between align-items-center mb-4">
color: white; <h2>Дашборд</h2>
background-color: rgba(255,255,255,0.1); <div>
} <span class="text-muted">Админ: {{ username }}</span>
<a href="/" class="btn btn-outline-primary btn-sm ms-2">
<i class="bi bi-arrow-clockwise"></i> Обновить
</a>
</div>
</div>
.sidebar .nav-link i { <!-- Stats Cards -->
margin-right: 0.5rem; <div class="row">
} <div class="col-md-3">
<a href="/users?filter=all" style="text-decoration: none;">
.stat-card { <div class="stat-card primary">
border-radius: 10px; <div class="stat-value">{{ stats.users }}</div>
padding: 1.5rem; <div class="stat-label">Пользователей</div>
color: white;
margin-bottom: 1rem;
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card.primary { background: linear-gradient(135deg, var(--secondary-color), #2980b9); }
.stat-card.success { background: linear-gradient(135deg, var(--success-color), #229954); }
.stat-card.warning { background: linear-gradient(135deg, var(--warning-color), #d68910); }
.stat-card.danger { background: linear-gradient(135deg, var(--danger-color), #c0392b); }
.stat-card .stat-value {
font-size: 2.5rem;
font-weight: bold;
}
.stat-card .stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.content {
padding: 2rem;
}
.card {
border: none;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
margin-bottom: 1.5rem;
}
.card-header {
background-color: white;
border-bottom: 2px solid #f0f0f0;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<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>
<small>Админ Панель v1.11</small>
</div>
<nav class="nav flex-column">
<a class="nav-link active" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a>
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a>
<a class="nav-link" href="/verification">
<i class="bi bi-shield-check"></i> Верификация
{% if pending_count and pending_count > 0 %}
<span class="badge bg-danger ms-2">{{ pending_count }}</span>
{% endif %}
</a>
<a class="nav-link" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></i> Расписания</a>
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
</nav>
</div> </div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=verified" style="text-decoration: none;">
<div class="stat-card success">
<div class="stat-value">{{ stats.verified }}</div>
<div class="stat-label">Верифицировано</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=ig" style="text-decoration: none;">
<div class="stat-card info">
<div class="stat-value">{{ stats.ig }}</div>
<div class="stat-label">Инициативная группа</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=active_unverified" style="text-decoration: none;">
<div class="stat-card danger">
<div class="stat-value">{{ stats.active_unverified }}</div>
<div class="stat-label">Активных (50+ сообщ.)</div>
</div>
</a>
</div>
</div>
<!-- Main Content --> <!-- Additional Stats -->
<div class="col-md-10 content"> <div class="row mt-3">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="col-md-6">
<h2>Дашборд</h2> <div class="card">
<div> <div class="card-header">
<span class="text-muted">Админ: {{ username }}</span> <i class="bi bi-megaphone"></i> Объявления
<a href="/" class="btn btn-outline-primary btn-sm ms-2"> </div>
<i class="bi bi-arrow-clockwise"></i> Обновить <div class="card-body">
</a> <h3 class="text-center">{{ stats.ads }}</h3>
</div> <p class="text-center text-muted">Активных объявлений</p>
</div>
<!-- Stats Cards -->
<div class="row">
<div class="col-md-3">
<a href="/users?filter=all" style="text-decoration: none;">
<div class="stat-card primary">
<div class="stat-value">{{ stats.users }}</div>
<div class="stat-label">Пользователей</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=verified" style="text-decoration: none;">
<div class="stat-card success">
<div class="stat-value">{{ stats.verified }}</div>
<div class="stat-label">Верифицировано</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=ig" style="text-decoration: none;">
<div class="stat-card" style="background: linear-gradient(135deg, #17a2b8, #117a8b);">
<div class="stat-value">{{ stats.ig }}</div>
<div class="stat-label">Инициативная группа</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/users?filter=active_unverified" style="text-decoration: none;">
<div class="stat-card danger">
<div class="stat-value">{{ stats.active_unverified }}</div>
<div class="stat-label">Активных (50+ сообщ.)</div>
</div>
</a>
</div>
</div>
<!-- Additional Stats -->
<div class="row mt-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-megaphone"></i> Объявления
</div>
<div class="card-body">
<h3 class="text-center">{{ stats.ads }}</h3>
<p class="text-center text-muted">Активных объявлений</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> События
</div>
<div class="card-body">
<h3 class="text-center">{{ stats.schedules }}</h3>
<p class="text-center text-muted">Предстоящих событий</p>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-lightning"></i> Быстрые действия
</div>
<div class="card-body">
<a href="/users" class="btn btn-primary me-2">
<i class="bi bi-people"></i> Пользователи
</a>
<a href="/export" class="btn btn-success me-2">
<i class="bi bi-download"></i> Экспорт JSON
</a>
<i class="bi bi-gear"></i> Настройки
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-calendar-event"></i> События
</div>
<div class="card-body">
<h3 class="text-center">{{ stats.schedules }}</h3>
<p class="text-center text-muted">Предстоящих событий</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Quick Actions -->
</body> <div class="card mt-4">
</html> <div class="card-header">
<i class="bi bi-lightning"></i> Быстрые действия
</div>
<div class="card-body">
<a href="/users" class="btn btn-primary me-2">
<i class="bi bi-people"></i> Пользователи
</a>
<a href="/broadcast" class="btn btn-success me-2">
<i class="bi bi-broadcast"></i> Рассылки
</a>
<a href="/digests" class="btn btn-info me-2">
<i class="bi bi-newspaper"></i> Дайджесты
</a>
<a href="/export" class="btn btn-secondary me-2">
<i class="bi bi-download"></i> Экспорт
</a>
</div>
</div>
{% endblock %}

View file

@ -1,109 +1,80 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head>
<meta charset="UTF-8">
<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" 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="/ads"><i class="bi bi-megaphone"></i> Объявления</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link active" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></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"><i class="bi bi-calendar-event"></i> События дома</h2>
<div class="card mb-4"> {% block title %}События - Домовой Бот{% endblock %}
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить событие</h5>
</div>
<div class="card-body">
<form id="addEventForm" class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" id="eventTitle" placeholder="Название события" required>
</div>
<div class="col-md-3">
<select class="form-select" id="eventType">
<option value="meeting">👥 Собрание</option>
<option value="cleanup">🧹 Субботник</option>
<option value="holiday">🎉 Праздник</option>
<option value="other">📅 Другое</option>
</select>
</div>
<div class="col-md-3">
<input type="datetime-local" class="form-control" id="eventDate" required>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
<div class="col-md-12">
<textarea class="form-control" id="eventDesc" placeholder="Описание события" rows="2"></textarea>
</div>
</form>
</div>
</div>
<div class="card"> {% block nav_events %}active{% endblock %}
<div class="card-body">
<h5>Предстоящие события:</h5> {% block content %}
<table class="table table-hover"> <h2 class="mb-4"><i class="bi bi-calendar-event"></i> События дома</h2>
<thead>
<tr><th>Название</th><th>Тип</th><th>Дата и время</th><th>Описание</th></tr> <div class="card mb-4">
</thead> <div class="card-header bg-primary text-white">
<tbody> <h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить событие</h5>
{% for event in events %}
<tr>
<td>{{ event.title }}</td>
<td>{{ event.event_type or '—' }}</td>
<td>{{ event.event_date.strftime('%d.%m.%Y %H:%M') if event.event_date else '—' }}</td>
<td>{{ event.description or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="card-body">
<script> <form id="addEventForm" class="row g-3">
document.getElementById('addEventForm').addEventListener('submit', async (e) => { <div class="col-md-4">
e.preventDefault(); <input type="text" class="form-control" id="eventTitle" placeholder="Название события" required>
const data = { </div>
title: document.getElementById('eventTitle').value, <div class="col-md-3">
event_type: document.getElementById('eventType').value, <select class="form-select" id="eventType">
event_date: new Date(document.getElementById('eventDate').value).toISOString(), <option value="meeting">👥 Собрание</option>
description: document.getElementById('eventDesc').value <option value="cleanup">🧹 Субботник</option>
}; <option value="holiday">🎉 Праздник</option>
const r = await fetch('/api/event/add', { <option value="other">📅 Другое</option>
method: 'POST', headers: {'Content-Type': 'application/json'}, </select>
body: JSON.stringify(data) </div>
}); <div class="col-md-3">
const result = await r.json(); <input type="datetime-local" class="form-control" id="eventDate" required>
if (result.success) { alert('✅ Событие добавлено'); location.reload(); } </div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
<div class="col-md-12">
<textarea class="form-control" id="eventDesc" placeholder="Описание события" rows="2"></textarea>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5>Предстоящие события:</h5>
<table class="table table-hover">
<thead>
<tr><th>Название</th><th>Тип</th><th>Дата и время</th><th>Описание</th></tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td>{{ event.title }}</td>
<td>{{ event.event_type or '—' }}</td>
<td>{{ event.event_date.strftime('%d.%m.%Y %H:%M') if event.event_date else '—' }}</td>
<td>{{ event.description or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('addEventForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
title: document.getElementById('eventTitle').value,
event_type: document.getElementById('eventType').value,
event_date: new Date(document.getElementById('eventDate').value).toISOString(),
description: document.getElementById('eventDesc').value
};
const r = await fetch('/api/event/add', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
}); });
</script> const result = await r.json();
</body> if (result.success) { alert('✅ Событие добавлено'); location.reload(); }
</html> });
</script>
{% endblock %}

View file

@ -1,103 +1,73 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head> {% block title %}Экспорт - Домовой Бот{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> {% block nav_export %}active{% endblock %}
<title>Экспорт - Домовой Бот</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> {% block content %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> <h2 class="mb-4">Экспорт данных</h2>
<style>
.sidebar { <div class="row">
min-height: 100vh; <div class="col-md-6">
background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%); <div class="card">
} <div class="card-header">
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; } <i class="bi bi-file-earmark-json"></i> Экспорт в JSON
.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" href="/users"><i class="bi bi-people"></i> Пользователи</a>
<a class="nav-link active" href="/export"><i class="bi bi-download"></i> Экспорт</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
</nav>
</div> </div>
<div class="card-body">
<div class="col-md-10 content"> <p>Экспортирует всех пользователей и сообщения в JSON формат.</p>
<h2 class="mb-4">Экспорт данных</h2> <button class="btn btn-primary" onclick="exportJSON()">
<i class="bi bi-download"></i> Экспортировать
<div class="row"> </button>
<div class="col-md-6"> <div id="exportResult" class="mt-3"></div>
<div class="card">
<div class="card-header">
<i class="bi bi-file-earmark-json"></i> Экспорт в JSON
</div>
<div class="card-body">
<p>Экспортирует всех пользователей и сообщения в JSON формат.</p>
<button class="btn btn-primary" onclick="exportJSON()">
<i class="bi bi-download"></i> Экспортировать
</button>
<div id="exportResult" class="mt-3"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<i class="bi bi-info-circle"></i> Информация
</div>
<div class="card-body">
<p>Экспорт включает:</p>
<ul>
<li>Всех пользователей</li>
<li>Все сообщения</li>
<li>Даты и время</li>
<li>Квартиры и телефоны</li>
</ul>
<p class="text-muted small">
Файл сохраняется в папке exports/
</p>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="col-md-6">
<script> <div class="card">
async function exportJSON() { <div class="card-header">
const resultDiv = document.getElementById('exportResult'); <i class="bi bi-info-circle"></i> Информация
resultDiv.innerHTML = '<div class="alert alert-info">Экспорт...</div>'; </div>
<div class="card-body">
<p>Экспорт включает:</p>
<ul>
<li>Всех пользователей</li>
<li>Все сообщения</li>
<li>Даты и время</li>
<li>Квартиры и телефоны</li>
</ul>
<p class="text-muted small">
Файл сохраняется в папке exports/
</p>
</div>
</div>
</div>
</div>
{% endblock %}
try { {% block extra_script %}
const response = await fetch('/api/export/json'); <script>
const data = await response.json(); async function exportJSON() {
const resultDiv = document.getElementById('exportResult');
resultDiv.innerHTML = '<div class="alert alert-info">Экспорт...</div>';
if (data.success) { try {
resultDiv.innerHTML = ` const response = await fetch('/api/export/json');
<div class="alert alert-success"> const data = await response.json();
✅ Экспорт успешно!<br>
Файл: ${data.filepath} if (data.success) {
</div> resultDiv.innerHTML = `
`; <div class="alert alert-success">
} else { ✅ Экспорт успешно!<br>
resultDiv.innerHTML = '<div class="alert alert-danger">Ошибка экспорта</div>'; Файл: ${data.filepath}
} </div>
} catch (error) { `;
resultDiv.innerHTML = `<div class="alert alert-danger">Ошибка: ${error.message}</div>`; } else {
resultDiv.innerHTML = '<div class="alert alert-danger">Ошибка экспорта</div>';
} }
} catch (error) {
resultDiv.innerHTML = `<div class="alert alert-danger">Ошибка: ${error.message}</div>`;
} }
</script> }
</body> </script>
</html> {% endblock %}

View file

@ -1,213 +1,187 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head>
<meta charset="UTF-8">
<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; }
.service-image-preview { max-width: 200px; max-height: 200px; object-fit: contain; }
</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" 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 active" href="/phones"><i class="bi bi-telephone"></i> Телефоны</a>
<a class="nav-link" href="/ads"><i class="bi bi-megaphone"></i> Объявления</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></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"><i class="bi bi-telephone"></i> Телефоны и службы</h2>
<!-- Добавить службу --> {% block title %}Телефоны и службы - Домовой Бот{% endblock %}
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить службу</h5>
</div>
<div class="card-body">
<form id="addServiceForm" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="serviceName" placeholder="Название" required>
</div>
<div class="col-md-2">
<input type="text" class="form-control" id="servicePhone" placeholder="Телефон" required>
</div>
<div class="col-md-2">
<select class="form-select" id="serviceCategory">
<option value="emergency">🚨 Экстренные</option>
<option value="utility">🔧 ЖКХ</option>
<option value="masters">👷 Мастера</option>
<option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="serviceDesc" placeholder="Описание">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
</form>
</div>
</div>
<!-- Загрузить картинку --> {% block nav_phones %}active{% endblock %}
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-image"></i> Загрузить картинку для службы</h5>
</div>
<div class="card-body">
<form id="uploadImageForm" class="row g-3" enctype="multipart/form-data">
<div class="col-md-3">
<select class="form-select" id="imageCategory" required>
<option value="emergency">🚨 Экстренные</option>
<option value="utility">🔧 ЖКХ</option>
<option value="masters">👷 Мастера</option>
<option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-4">
<input type="file" class="form-control" id="imageFile" accept="image/*" required>
<small class="text-muted">JPG, PNG до 5MB</small>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100">Загрузить</button>
</div>
</form>
<div id="uploadResult" class="mt-3"></div>
</div>
</div>
<!-- Список служб --> {% block extra_style %}
<div class="card"> .service-image-preview { max-width: 200px; max-height: 200px; object-fit: contain; }
<div class="card-body"> {% endblock %}
<h5>Существующие службы:</h5>
<table class="table table-hover"> {% block content %}
<thead> <h2 class="mb-4"><i class="bi bi-telephone"></i> Телефоны и службы</h2>
<tr>
<th>Категория</th> <!-- Добавить службу -->
<th>Название</th> <div class="card mb-4">
<th>Телефон</th> <div class="card-header bg-primary text-white">
<th>Описание</th> <h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить службу</h5>
<th>Картинка</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for service in services %}
<tr>
<td>
{% if service.category == 'emergency' %}
<span class="badge bg-danger">🚨 Экстренные</span>
{% elif service.category == 'utility' %}
<span class="badge bg-info">🔧 ЖКХ</span>
{% elif service.category == 'masters' %}
<span class="badge bg-success">👷 Мастера</span>
{% elif service.category == 'police' %}
<span class="badge bg-warning text-dark">👮 Полиция</span>
{% else %}
<span class="badge bg-secondary">Другое</span>
{% endif %}
</td>
<td>{{ service.name }}</td>
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
<td>{{ service.description or '—' }}</td>
<td>
{% if service.image_path %}
<img src="/static/service_images/{{ service.image_path.split('/')[-1] }}"
class="service-image-preview" alt="Картинка">
<br><small class="text-muted"></small>
{% else %}
<span class="text-muted">❌ Нет</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not services %}
<p class="text-center text-muted">Служб нет</p>
{% endif %}
</div>
</div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="card-body">
<script> <form id="addServiceForm" class="row g-3">
// Добавление службы <div class="col-md-3">
document.getElementById('addServiceForm').addEventListener('submit', async (e) => { <input type="text" class="form-control" id="serviceName" placeholder="Название" required>
e.preventDefault(); </div>
const data = { <div class="col-md-2">
name: document.getElementById('serviceName').value, <input type="text" class="form-control" id="servicePhone" placeholder="Телефон" required>
phone: document.getElementById('servicePhone').value, </div>
category: document.getElementById('serviceCategory').value, <div class="col-md-2">
description: document.getElementById('serviceDesc').value <select class="form-select" id="serviceCategory">
}; <option value="emergency">🚨 Экстренные</option>
const r = await fetch('/api/service/add', { <option value="utility">🔧 ЖКХ</option>
method: 'POST', headers: {'Content-Type': 'application/json'}, <option value="masters">👷 Мастера</option>
body: JSON.stringify(data) <option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-3">
<input type="text" class="form-control" id="serviceDesc" placeholder="Описание">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
</form>
</div>
</div>
<!-- Загрузить картинку -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-image"></i> Загрузить картинку для службы</h5>
</div>
<div class="card-body">
<form id="uploadImageForm" class="row g-3" enctype="multipart/form-data">
<div class="col-md-3">
<select class="form-select" id="imageCategory" required>
<option value="emergency">🚨 Экстренные</option>
<option value="utility">🔧 ЖКХ</option>
<option value="masters">👷 Мастера</option>
<option value="police">👮 Полиция</option>
<option value="other">📋 Другое</option>
</select>
</div>
<div class="col-md-4">
<input type="file" class="form-control" id="imageFile" accept="image/*" required>
<small class="text-muted">JPG, PNG до 5MB</small>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-success w-100">Загрузить</button>
</div>
</form>
<div id="uploadResult" class="mt-3"></div>
</div>
</div>
<!-- Список служб -->
<div class="card">
<div class="card-body">
<h5>Существующие службы:</h5>
<table class="table table-hover">
<thead>
<tr>
<th>Категория</th>
<th>Название</th>
<th>Телефон</th>
<th>Описание</th>
<th>Картинка</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for service in services %}
<tr>
<td>
{% if service.category == 'emergency' %}
<span class="badge bg-danger">🚨 Экстренные</span>
{% elif service.category == 'utility' %}
<span class="badge bg-info">🔧 ЖКХ</span>
{% elif service.category == 'masters' %}
<span class="badge bg-success">👷 Мастера</span>
{% elif service.category == 'police' %}
<span class="badge bg-warning text-dark">👮 Полиция</span>
{% else %}
<span class="badge bg-secondary">Другое</span>
{% endif %}
</td>
<td>{{ service.name }}</td>
<td><a href="tel:{{ service.phone }}">{{ service.phone }}</a></td>
<td>{{ service.description or '—' }}</td>
<td>
{% if service.image_path %}
<img src="/static/service_images/{{ service.image_path.split('/')[-1] }}"
class="service-image-preview" alt="Картинка">
<br><small class="text-muted"></small>
{% else %}
<span class="text-muted">❌ Нет</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteService({{ service.service_id }})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not services %}
<p class="text-center text-muted">Служб нет</p>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
// Добавление службы
document.getElementById('addServiceForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
name: document.getElementById('serviceName').value,
phone: document.getElementById('servicePhone').value,
category: document.getElementById('serviceCategory').value,
description: document.getElementById('serviceDesc').value
};
const r = await fetch('/api/service/add', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await r.json();
if (result.success) { alert('✅ Служба добавлена'); location.reload(); }
else { alert('❌ Ошибка: ' + (result.error || 'Неизвестная')); }
});
// Загрузка картинки
document.getElementById('uploadImageForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('category', document.getElementById('imageCategory').value);
formData.append('file', document.getElementById('imageFile').files[0]);
const resultDiv = document.getElementById('uploadResult');
resultDiv.innerHTML = '<div class="alert alert-info">Загрузка...</div>';
try {
const r = await fetch('/api/service/upload_image', {
method: 'POST',
body: formData
}); });
const result = await r.json(); const result = await r.json();
if (result.success) { alert('✅ Служба добавлена'); location.reload(); }
else { alert('❌ Ошибка: ' + (result.error || 'Неизвестная')); }
});
// Загрузка картинки if (result.success) {
document.getElementById('uploadImageForm').addEventListener('submit', async (e) => { resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}<br>Файл: ${result.file_path}</div>`;
e.preventDefault(); } else {
const formData = new FormData(); resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error}</div>`;
formData.append('category', document.getElementById('imageCategory').value);
formData.append('file', document.getElementById('imageFile').files[0]);
const resultDiv = document.getElementById('uploadResult');
resultDiv.innerHTML = '<div class="alert alert-info">Загрузка...</div>';
try {
const r = await fetch('/api/service/upload_image', {
method: 'POST',
body: formData
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}<br>Файл: ${result.file_path}</div>`;
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error}</div>`;
}
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
} }
}); } catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
async function deleteService(id) {
if (!confirm('Удалить службу?')) return;
const r = await fetch(`/api/service/${id}/delete`, { method: 'POST' });
const result = await r.json();
if (result.success) { alert('✅ Удалено'); location.reload(); }
} }
</script> });
</body>
</html> async function deleteService(id) {
if (!confirm('Удалить службу?')) return;
const r = await fetch(`/api/service/${id}/delete`, { method: 'POST' });
const result = await r.json();
if (result.success) { alert('✅ Удалено'); location.reload(); }
}
</script>
{% endblock %}

View file

@ -1,113 +1,84 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head>
<meta charset="UTF-8">
<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" 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="/ads"><i class="bi bi-megaphone"></i> Объявления</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link active" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></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"><i class="bi bi-bar-chart"></i> Опросы и голосования</h2>
<div class="card mb-4"> {% block title %}Опросы - Домовой Бот{% endblock %}
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Создать опрос</h5>
</div>
<div class="card-body">
<form id="createPollForm" class="row g-3">
<div class="col-md-12">
<input type="text" class="form-control" id="pollQuestion" placeholder="Вопрос" required>
</div>
<div class="col-md-12">
<label class="form-label">Варианты ответов (каждый с новой строки):</label>
<textarea class="form-control" id="pollOptions" rows="4" placeholder="Вариант 1&#10;Вариант 2&#10;Вариант 3" required></textarea>
</div>
<div class="col-md-12">
<button type="submit" class="btn btn-primary">Создать опрос</button>
</div>
</form>
</div>
</div>
<div class="card"> {% block nav_polls %}active{% endblock %}
<div class="card-body">
<h5>Активные опросы:</h5> {% block content %}
<table class="table table-hover"> <h2 class="mb-4"><i class="bi bi-bar-chart"></i> Опросы и голосования</h2>
<thead>
<tr><th>Вопрос</th><th>Варианты</th><th>Голосов</th><th>Дата</th><th>Действия</th></tr> <div class="card mb-4">
</thead> <div class="card-header bg-primary text-white">
<tbody> <h5 class="mb-0"><i class="bi bi-plus-lg"></i> Создать опрос</h5>
{% for poll in polls %}
<tr>
<td>{{ poll.question }}</td>
<td>{{ (poll.options|length) if poll.options else 0 }} варианта</td>
<td>{{ poll.votes|length if poll.votes else 0 }}</td>
<td>{{ poll.created_at.strftime('%d.%m.%Y') if poll.created_at else '—' }}</td>
<td>
{% if poll.is_active %}
<button class="btn btn-sm btn-warning" onclick="closePoll({{ poll.poll_id }})">
<i class="bi bi-lock"></i> Закрыть
</button>
{% else %}
<span class="badge bg-secondary">Завершён</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="card-body">
<script> <form id="createPollForm" class="row g-3">
document.getElementById('createPollForm').addEventListener('submit', async (e) => { <div class="col-md-12">
e.preventDefault(); <input type="text" class="form-control" id="pollQuestion" placeholder="Вопрос" required>
const question = document.getElementById('pollQuestion').value; </div>
const optionsText = document.getElementById('pollOptions').value; <div class="col-md-12">
const options = optionsText.split('\n').filter(o => o.trim()); <label class="form-label">Варианты ответов (каждый с новой строки):</label>
<textarea class="form-control" id="pollOptions" rows="4" placeholder="Вариант 1&#10;Вариант 2&#10;Вариант 3" required></textarea>
</div>
<div class="col-md-12">
<button type="submit" class="btn btn-primary">Создать опрос</button>
</div>
</form>
</div>
</div>
const r = await fetch('/api/poll/create', { <div class="card">
method: 'POST', <div class="card-body">
headers: {'Content-Type': 'application/json'}, <h5>Активные опросы:</h5>
body: JSON.stringify({ question, options, is_active: true }) <table class="table table-hover">
}); <thead>
const result = await r.json(); <tr><th>Вопрос</th><th>Варианты</th><th>Голосов</th><th>Дата</th><th>Действия</th></tr>
if (result.success) { alert('✅ Опрос создан'); location.reload(); } </thead>
<tbody>
{% for poll in polls %}
<tr>
<td>{{ poll.question }}</td>
<td>{{ (poll.options|length) if poll.options else 0 }} варианта</td>
<td>{{ poll.votes|length if poll.votes else 0 }}</td>
<td>{{ poll.created_at.strftime('%d.%m.%Y') if poll.created_at else '—' }}</td>
<td>
{% if poll.is_active %}
<button class="btn btn-sm btn-warning" onclick="closePoll({{ poll.poll_id }})">
<i class="bi bi-lock"></i> Закрыть
</button>
{% else %}
<span class="badge bg-secondary">Завершён</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('createPollForm').addEventListener('submit', async (e) => {
e.preventDefault();
const question = document.getElementById('pollQuestion').value;
const optionsText = document.getElementById('pollOptions').value;
const options = optionsText.split('\n').filter(o => o.trim());
const r = await fetch('/api/poll/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ question, options, is_active: true })
}); });
async function closePoll(id) { const result = await r.json();
if (!confirm('Закрыть опрос?')) return; if (result.success) { alert('✅ Опрос создан'); location.reload(); }
const r = await fetch(`/api/poll/${id}/close`, { method: 'POST' }); });
const result = await r.json(); async function closePoll(id) {
if (result.success) { alert('✅ Опрос закрыт'); location.reload(); } if (!confirm('Закрыть опрос?')) return;
} const r = await fetch(`/api/poll/${id}/close`, { method: 'POST' });
</script> const result = await r.json();
</body> if (result.success) { alert('✅ Опрос закрыт'); location.reload(); }
</html> }
</script>
{% endblock %}

View file

@ -1,296 +1,269 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head> {% block title %}Запланированные посты - Домовой Бот{% endblock %}
<meta charset="UTF-8">
<title>Запланированные посты - Домовой Бот</title> {% block nav_scheduled_posts %}active{% endblock %}
<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"> {% block extra_style %}
<style> #preview { max-width: 400px; margin-top: 1rem; display: none; }
.sidebar { min-height: 100vh; background: linear-gradient(180deg, #2c3e50 0%, #1a252f 100%); } #preview img { max-width: 100%; border-radius: 8px; }
.sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 1rem; } .post-card { transition: transform 0.2s; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: white; background-color: rgba(255,255,255,0.1); } .post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
.content { padding: 2rem; } {% endblock %}
#preview { max-width: 400px; margin-top: 1rem; display: none; }
#preview img { max-width: 100%; border-radius: 8px; } {% block content %}
.post-card { transition: transform 0.2s; } <h2 class="mb-4"><i class="bi bi-calendar2-plus"></i> Запланированные посты</h2>
.post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
</style> <!-- Форма создания поста -->
</head> <div class="card mb-4">
<body> <div class="card-header bg-primary text-white">
<div class="container-fluid"> <h5 class="mb-0"><i class="bi bi-plus-circle"></i> Создать новый пост</h5>
<div class="row"> </div>
<div class="col-md-2 sidebar p-0"> <div class="card-body">
<div class="p-3 text-white text-center"> <form id="scheduledPostForm" enctype="multipart/form-data">
<h4><i class="bi bi-house-door"></i> Домовой Бот</h4> <div class="mb-3">
</div> <label class="form-label">Текст поста</label>
<nav class="nav flex-column"> <textarea class="form-control" id="postText" rows="5"
<a class="nav-link" href="/"><i class="bi bi-speedometer2"></i> Дашборд</a> placeholder="Введите текст поста..." required></textarea>
<a class="nav-link" href="/users"><i class="bi bi-people"></i> Пользователи</a> <small class="text-muted">Поддерживается HTML: &lt;b&gt;жирный&lt;/b&gt;, &lt;i&gt;курсив&lt;/i&gt;, &lt;emoji&gt;</small>
<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="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link active" href="/scheduled_posts"><i class="bi bi-calendar-event"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link" href="/schedules"><i class="bi bi-clock"></i> Расписания</a>
<a class="nav-link" href="/export"><i class="bi bi-download"></i> Экспорт</a>
</nav>
</div> </div>
<div class="col-md-10 content">
<h2 class="mb-4"><i class="bi bi-calendar2-plus"></i> Запланированные посты</h2>
<!-- Форма создания поста --> <div class="mb-3">
<div class="card mb-4"> <label class="form-label">Картинка (необязательно)</label>
<div class="card-header bg-primary text-white"> <input type="file" class="form-control" id="postPhoto" accept="image/*"
<h5 class="mb-0"><i class="bi bi-plus-circle"></i> Создать новый пост</h5> onchange="previewImage()">
</div> <small class="text-muted">JPG, PNG до 5MB</small>
<div class="card-body"> <div id="preview">
<form id="scheduledPostForm" enctype="multipart/form-data"> <img id="previewImg" alt="Предпросмотр">
<div class="mb-3">
<label class="form-label">Текст поста</label>
<textarea class="form-control" id="postText" rows="5"
placeholder="Введите текст поста..." required></textarea>
<small class="text-muted">Поддерживается HTML: &lt;b&gt;жирный&lt;/b&gt;, &lt;i&gt;курсив&lt;/i&gt;, &lt;emoji&gt;</small>
</div>
<div class="mb-3">
<label class="form-label">Картинка (необязательно)</label>
<input type="file" class="form-control" id="postPhoto" accept="image/*"
onchange="previewImage()">
<small class="text-muted">JPG, PNG до 5MB</small>
<div id="preview">
<img id="previewImg" alt="Предпросмотр">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Тема форума</label>
<select class="form-select" id="postTopic" required>
<option value="general">💬 Общий чат</option>
<option value="memes">😂 Мемы</option>
<option value="ads">📢 Важные объявления</option>
<option value="meetings">🏛️ Собрания собственников</option>
<option value="kgm">🗑️ Вывоз КГМ</option>
<option value="lostfound">🔍 Потеряшки</option>
<option value="auto">🚗 Для автовладельцев</option>
<option value="phones">📞 Важные телефоны</option>
<option value="links">🔗 Ссылки</option>
<option value="uo">📝 Переписка с УО</option>
</select>
<small class="text-muted">В какую тему отправить пост</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Получатели</label>
<select class="form-select" id="postRecipients">
<option value="chat_only">📢 Только в чат</option>
<option value="all_verified">✅ Всем верифицированным в личку</option>
<option value="all_and_chat">✅ Всем + в чат</option>
</select>
<small class="text-muted">Кому отправить пост</small>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Дата публикации</label>
<input type="date" class="form-control" id="postDate" required>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Время публикации</label>
<input type="time" class="form-control" id="postTime" required>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<b>Часовой пояс:</b> Все времена указаны в твоём локальном времени (UTC+4, Екатеринбург/Ульяновск).
<br>Бот автоматически конвертирует время в UTC для публикации по расписанию.
</div>
<button type="submit" class="btn btn-success btn-lg">
<i class="bi bi-calendar-check"></i> Запланировать пост
</button>
</form>
<div id="result" class="mt-4"></div>
</div>
</div>
<!-- Список запланированных постов -->
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-list-check"></i> Запланированные посты</h5>
</div>
<div class="card-body">
<div id="postsList"></div>
<div id="noPosts" class="text-center text-muted py-5">
<i class="bi bi-calendar-x" style="font-size: 3rem;"></i>
<p class="mt-3">Пока нет запланированных постов</p>
</div>
</div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Тема форума</label>
<select class="form-select" id="postTopic" required>
<option value="general">💬 Общий чат</option>
<option value="memes">😂 Мемы</option>
<option value="ads">📢 Важные объявления</option>
<option value="meetings">🏛️ Собрания собственников</option>
<option value="kgm">🗑️ Вывоз КГМ</option>
<option value="lostfound">🔍 Потеряшки</option>
<option value="auto">🚗 Для автовладельцев</option>
<option value="phones">📞 Важные телефоны</option>
<option value="links">🔗 Ссылки</option>
<option value="uo">📝 Переписка с УО</option>
</select>
<small class="text-muted">В какую тему отправить пост</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Получатели</label>
<select class="form-select" id="postRecipients">
<option value="chat_only">📢 Только в чат</option>
<option value="all_verified">✅ Всем верифицированным в личку</option>
<option value="all_and_chat">✅ Всем + в чат</option>
</select>
<small class="text-muted">Кому отправить пост</small>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Дата публикации</label>
<input type="date" class="form-control" id="postDate" required>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Время публикации</label>
<input type="time" class="form-control" id="postTime" required>
<small class="text-muted">🕐 По твоему времени (UTC+4)</small>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<b>Часовой пояс:</b> Все времена указаны в твоём локальном времени (UTC+4, Екатеринбург/Ульяновск).
<br>Бот автоматически конвертирует время в UTC для публикации по расписанию.
</div>
<button type="submit" class="btn btn-success btn-lg">
<i class="bi bi-calendar-check"></i> Запланировать пост
</button>
</form>
<div id="result" class="mt-4"></div>
</div>
</div>
<!-- Список запланированных постов -->
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-list-check"></i> Запланированные посты</h5>
</div>
<div class="card-body">
<div id="postsList"></div>
<div id="noPosts" class="text-center text-muted py-5">
<i class="bi bi-calendar-x" style="font-size: 3rem;"></i>
<p class="mt-3">Пока нет запланированных постов</p>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> </div>
<script> {% endblock %}
// Установка минимальной даты (сегодня)
document.addEventListener('DOMContentLoaded', function() {
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
document.getElementById('postDate').setAttribute('min', todayStr);
// Показываем текущее время в браузере пользователя {% block extra_script %}
const tzOffset = -today.getTimezoneOffset() / 60; <script>
const tzLabel = tzOffset >= 0 ? `UTC+${tzOffset}` : `UTC${tzOffset}`; // Установка минимальной даты (сегодня)
console.log(`🕐 Твой часовой пояс: ${tzLabel}`); document.addEventListener('DOMContentLoaded', function() {
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
document.getElementById('postDate').setAttribute('min', todayStr);
loadPosts(); // Показываем текущее время в браузере пользователя
}); const tzOffset = -today.getTimezoneOffset() / 60;
const tzLabel = tzOffset >= 0 ? `UTC+${tzOffset}` : `UTC${tzOffset}`;
console.log(`🕐 Твой часовой пояс: ${tzLabel}`);
function previewImage() { loadPosts();
const file = document.getElementById('postPhoto').files[0]; });
const preview = document.getElementById('preview');
const previewImg = document.getElementById('previewImg');
if (file) { function previewImage() {
const reader = new FileReader(); const file = document.getElementById('postPhoto').files[0];
reader.onload = function(e) { const preview = document.getElementById('preview');
previewImg.src = e.target.result; const previewImg = document.getElementById('previewImg');
preview.style.display = 'block';
} if (file) {
reader.readAsDataURL(file); const reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(file);
} else {
preview.style.display = 'none';
}
}
// Отправка формы
document.getElementById('scheduledPostForm').addEventListener('submit', async (e) => {
e.preventDefault();
const text = document.getElementById('postText').value;
const photo = document.getElementById('postPhoto').files[0];
const topic = document.getElementById('postTopic').value;
const recipients = document.getElementById('postRecipients').value;
const date = document.getElementById('postDate').value;
const time = document.getElementById('postTime').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) {
alert('Введите текст поста!');
return;
}
if (!date || !time) {
alert('Выберите дату и время!');
return;
}
const scheduledTime = `${date}T${time}:00`;
const formData = new FormData();
formData.append('text', text);
formData.append('topic_name', topic);
formData.append('recipients', recipients);
formData.append('scheduled_time', scheduledTime);
if (photo) {
formData.append('photo', photo);
}
try {
const r = await fetch('/api/scheduled_posts/create', {
method: 'POST',
body: formData
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}</div>`;
document.getElementById('scheduledPostForm').reset();
document.getElementById('preview').style.display = 'none';
loadPosts();
} else { } else {
preview.style.display = 'none'; resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
} }
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
} }
});
// Отправка формы // Загрузка постов
document.getElementById('scheduledPostForm').addEventListener('submit', async (e) => { async function loadPosts() {
e.preventDefault(); try {
const r = await fetch('/api/scheduled_posts/list');
const result = await r.json();
const text = document.getElementById('postText').value; const postsList = document.getElementById('postsList');
const photo = document.getElementById('postPhoto').files[0]; const noPosts = document.getElementById('noPosts');
const topic = document.getElementById('postTopic').value;
const recipients = document.getElementById('postRecipients').value;
const date = document.getElementById('postDate').value;
const time = document.getElementById('postTime').value;
const resultDiv = document.getElementById('result');
if (!text.trim()) { if (result.success && result.posts.length > 0) {
alert('Введите текст поста!'); noPosts.style.display = 'none';
return; postsList.innerHTML = '';
}
if (!date || !time) { result.posts.forEach(post => {
alert('Выберите дату и время!'); const card = document.createElement('div');
return; card.className = 'card mb-3 post-card';
} card.innerHTML = `
<div class="card-body">
const scheduledTime = `${date}T${time}:00`; <div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-1">${post.topic_emoji} ${post.topic_name || 'Без темы'}</h6>
const formData = new FormData(); <span class="badge bg-${post.status === 'pending' ? 'warning' : post.status === 'sent' ? 'success' : post.status === 'failed' ? 'danger' : 'secondary'}">
formData.append('text', text); ${post.status_emoji} ${post.status}
formData.append('topic_name', topic); </span>
formData.append('recipients', recipients);
formData.append('scheduled_time', scheduledTime);
if (photo) {
formData.append('photo', photo);
}
try {
const r = await fetch('/api/scheduled_posts/create', {
method: 'POST',
body: formData
});
const result = await r.json();
if (result.success) {
resultDiv.innerHTML = `<div class="alert alert-success">✅ ${result.message}</div>`;
document.getElementById('scheduledPostForm').reset();
document.getElementById('preview').style.display = 'none';
loadPosts();
} else {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${result.error || 'Неизвестная'}</div>`;
}
} catch (err) {
resultDiv.innerHTML = `<div class="alert alert-danger">❌ Ошибка: ${err.message}</div>`;
}
});
// Загрузка постов
async function loadPosts() {
try {
const r = await fetch('/api/scheduled_posts/list');
const result = await r.json();
const postsList = document.getElementById('postsList');
const noPosts = document.getElementById('noPosts');
if (result.success && result.posts.length > 0) {
noPosts.style.display = 'none';
postsList.innerHTML = '';
result.posts.forEach(post => {
const card = document.createElement('div');
card.className = 'card mb-3 post-card';
card.innerHTML = `
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-1">${post.topic_emoji} ${post.topic_name || 'Без темы'}</h6>
<span class="badge bg-${post.status === 'pending' ? 'warning' : post.status === 'sent' ? 'success' : post.status === 'failed' ? 'danger' : 'secondary'}">
${post.status_emoji} ${post.status}
</span>
</div>
<p class="mb-2">${post.text.substring(0, 100)}${post.text.length > 100 ? '...' : ''}</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
📅 ${post.scheduled_time}
</small>
${post.status === 'pending' ? `
<button class="btn btn-sm btn-danger" onclick="cancelPost(${post.id})">
<i class="bi bi-x-circle"></i> Отменить
</button>
` : ''}
</div>
</div> </div>
`; <p class="mb-2">${post.text.substring(0, 100)}${post.text.length > 100 ? '...' : ''}</p>
postsList.appendChild(card); <div class="d-flex justify-content-between align-items-center">
}); <small class="text-muted">
} else { 📅 ${post.scheduled_time}
noPosts.style.display = 'block'; </small>
postsList.innerHTML = ''; ${post.status === 'pending' ? `
} <button class="btn btn-sm btn-danger" onclick="cancelPost(${post.id})">
} catch (err) { <i class="bi bi-x-circle"></i> Отменить
console.error('Ошибка загрузки постов:', err); </button>
} ` : ''}
} </div>
</div>
// Отмена поста `;
async function cancelPost(postId) { postsList.appendChild(card);
if (!confirm('Отменить этот пост?')) return;
try {
const r = await fetch(`/api/scheduled_posts/${postId}/cancel`, {
method: 'POST'
}); });
const result = await r.json(); } else {
noPosts.style.display = 'block';
if (result.success) { postsList.innerHTML = '';
alert('✅ Пост отменён');
loadPosts();
} else {
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
} }
} catch (err) {
console.error('Ошибка загрузки постов:', err);
} }
</script> }
</body>
</html> // Отмена поста
async function cancelPost(postId) {
if (!confirm('Отменить этот пост?')) return;
try {
const r = await fetch(`/api/scheduled_posts/${postId}/cancel`, {
method: 'POST'
});
const result = await r.json();
if (result.success) {
alert('✅ Пост отменён');
loadPosts();
} else {
alert('❌ Ошибка: ' + (result.error || 'Неизвестная'));
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
}
}
</script>
{% endblock %}

View file

@ -1,133 +1,104 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="ru">
<head>
<meta charset="UTF-8">
<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" 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="/ads"><i class="bi bi-megaphone"></i> Объявления</a>
<a class="nav-link" href="/broadcast"><i class="bi bi-broadcast"></i> Рассылки</a>
<a class="nav-link" href="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
<a class="nav-link" href="/polls"><i class="bi bi-bar-chart"></i> Опросы</a>
<a class="nav-link" href="/events"><i class="bi bi-calendar-event"></i> События</a>
<a class="nav-link active" href="/schedules"><i class="bi bi-clock"></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"><i class="bi bi-clock"></i> Расписания отключений</h2>
<div class="card mb-4"> {% block title %}Расписания - Домовой Бот{% endblock %}
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить расписание</h5>
</div>
<div class="card-body">
<form id="addScheduleForm" class="row g-3">
<div class="col-md-3">
<input type="text" class="form-control" id="scheduleTitle" placeholder="Название" required>
</div>
<div class="col-md-2">
<select class="form-select" id="scheduleType">
<option value="water">💧 Вода</option>
<option value="electricity">⚡ Электричество</option>
<option value="heating">🔥 Отопление</option>
<option value="gas">🔶 Газ</option>
<option value="elevator">🛗 Лифт</option>
<option value="general">📋 Общее</option>
</select>
</div>
<div class="col-md-2">
<input type="datetime-local" class="form-control" id="scheduleStart" required>
</div>
<div class="col-md-2">
<input type="datetime-local" class="form-control" id="scheduleEnd">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
<div class="col-md-12">
<textarea class="form-control" id="scheduleDesc" placeholder="Описание" rows="2"></textarea>
</div>
</form>
</div>
</div>
<div class="card"> {% block nav_schedules %}active{% endblock %}
<div class="card-body">
<table class="table table-hover"> {% block content %}
<thead> <h2 class="mb-4"><i class="bi bi-clock"></i> Расписания отключений</h2>
<tr><th>Тип</th><th>Название</th><th>Начало</th><th>Конец</th><th>Описание</th><th>Действия</th></tr>
</thead> <div class="card mb-4">
<tbody> <div class="card-header bg-primary text-white">
{% for schedule in schedules %} <h5 class="mb-0"><i class="bi bi-plus-lg"></i> Добавить расписание</h5>
<tr>
<td>
{% if schedule.schedule_type == 'water' %}💧
{% elif schedule.schedule_type == 'electricity' %}⚡
{% elif schedule.schedule_type == 'heating' %}🔥
{% elif schedule.schedule_type == 'gas' %}🔶
{% elif schedule.schedule_type == 'elevator' %}🛗
{% else %}📋{% endif %}
</td>
<td>{{ schedule.title }}</td>
<td>{{ schedule.start_time.strftime('%d.%m.%Y %H:%M') if schedule.start_time else '—' }}</td>
<td>{{ schedule.end_time.strftime('%d.%m.%Y %H:%M') if schedule.end_time else '—' }}</td>
<td>{{ schedule.description or '—' }}</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteSchedule({{ schedule.schedule_id }})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <div class="card-body">
<script> <form id="addScheduleForm" class="row g-3">
document.getElementById('addScheduleForm').addEventListener('submit', async (e) => { <div class="col-md-3">
e.preventDefault(); <input type="text" class="form-control" id="scheduleTitle" placeholder="Название" required>
const data = { </div>
title: document.getElementById('scheduleTitle').value, <div class="col-md-2">
schedule_type: document.getElementById('scheduleType').value, <select class="form-select" id="scheduleType">
start_time: new Date(document.getElementById('scheduleStart').value).toISOString(), <option value="water">💧 Вода</option>
end_time: document.getElementById('scheduleEnd').value ? new Date(document.getElementById('scheduleEnd').value).toISOString() : null, <option value="electricity">⚡ Электричество</option>
description: document.getElementById('scheduleDesc').value, <option value="heating">🔥 Отопление</option>
is_active: true <option value="gas">🔶 Газ</option>
}; <option value="elevator">🛗 Лифт</option>
const r = await fetch('/api/schedule/add', { <option value="general">📋 Общее</option>
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) </select>
}); </div>
const result = await r.json(); <div class="col-md-2">
if (result.success) { alert('✅ Расписание добавлено'); location.reload(); } <input type="datetime-local" class="form-control" id="scheduleStart" required>
</div>
<div class="col-md-2">
<input type="datetime-local" class="form-control" id="scheduleEnd">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100">Добавить</button>
</div>
<div class="col-md-12">
<textarea class="form-control" id="scheduleDesc" placeholder="Описание" rows="2"></textarea>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr><th>Тип</th><th>Название</th><th>Начало</th><th>Конец</th><th>Описание</th><th>Действия</th></tr>
</thead>
<tbody>
{% for schedule in schedules %}
<tr>
<td>
{% if schedule.schedule_type == 'water' %}💧
{% elif schedule.schedule_type == 'electricity' %}⚡
{% elif schedule.schedule_type == 'heating' %}🔥
{% elif schedule.schedule_type == 'gas' %}🔶
{% elif schedule.schedule_type == 'elevator' %}🛗
{% else %}📋{% endif %}
</td>
<td>{{ schedule.title }}</td>
<td>{{ schedule.start_time.strftime('%d.%m.%Y %H:%M') if schedule.start_time else '—' }}</td>
<td>{{ schedule.end_time.strftime('%d.%m.%Y %H:%M') if schedule.end_time else '—' }}</td>
<td>{{ schedule.description or '—' }}</td>
<td>
<button class="btn btn-sm btn-danger" onclick="deleteSchedule({{ schedule.schedule_id }})">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('addScheduleForm').addEventListener('submit', async (e) => {
e.preventDefault();
const data = {
title: document.getElementById('scheduleTitle').value,
schedule_type: document.getElementById('scheduleType').value,
start_time: new Date(document.getElementById('scheduleStart').value).toISOString(),
end_time: document.getElementById('scheduleEnd').value ? new Date(document.getElementById('scheduleEnd').value).toISOString() : null,
description: document.getElementById('scheduleDesc').value,
is_active: true
};
const r = await fetch('/api/schedule/add', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)
}); });
async function deleteSchedule(id) { const result = await r.json();
if (!confirm('Удалить расписание?')) return; if (result.success) { alert('✅ Расписание добавлено'); location.reload(); }
const r = await fetch(`/api/schedule/${id}/delete`, { method: 'POST' }); });
const result = await r.json(); async function deleteSchedule(id) {
if (result.success) { alert('✅ Удалено'); location.reload(); } if (!confirm('Удалить расписание?')) return;
} const r = await fetch(`/api/schedule/${id}/delete`, { method: 'POST' });
</script> const result = await r.json();
</body> if (result.success) { alert('✅ Удалено'); location.reload(); }
</html> }
</script>
{% endblock %}

View file

@ -1,540 +1,234 @@
<!DOCTYPE html> {% extends "base.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="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
</nav>
</div>
<div class="col-md-10 content"> {% block title %}Пользователи - Домовой Бот{% endblock %}
<h2 class="mb-4">
Пользователи {% block nav_users %}active{% endblock %}
{% if current_filter %}
<span class="badge bg-info ms-2"> {% block content %}
{% if current_filter == 'verified' %} <h2 class="mb-4">
✅ Верифицированные Пользователи
{% elif current_filter == 'unverified' %} {% if current_filter %}
Не верифицированы <span class="badge bg-info ms-2">
{% elif current_filter == 'ig' %} {% if current_filter == 'verified' %}✅ Верифицированные
🔵 Инициативная группа {% elif current_filter == 'unverified' %}⏳ Не верифицированы
{% elif current_filter == 'active_unverified' %} {% elif current_filter == 'ig' %}🔵 Инициативная группа
🔥 Активные (50+ сообщ.) {% 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 %} {% else %}
Все <button class="btn btn-sm btn-danger" onclick="banUser({{ user.user_id }})"><i class="bi bi-lock"></i></button>
{% endif %} {% endif %}
</span> {% if user.verified %}
<a href="/users" class="btn btn-sm btn-outline-secondary ms-2"> <button class="btn btn-sm btn-warning" onclick="unverifyUser({{ user.user_id }})"><i class="bi bi-shield-x"></i></button>
<i class="bi bi-x"></i> Сбросить фильтр {% else %}
</a> <button class="btn btn-sm btn-success" onclick="verifyUser({{ user.user_id }})"><i class="bi bi-shield-check"></i></button>
{% 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 %} {% endif %}
</div> {% if user.is_admin %}
</div> <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>
<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> </div>
</div>
{% endblock %}
<!-- Modal для редактирования --> {% block extra_script %}
<div class="modal fade" id="editUserModal" tabindex="-1"> <script>
<div class="modal-dialog"> async function quickVerify(userId, apartment) {
<div class="modal-content"> if (!confirm(`Верифицировать пользователя (кв. ${apartment})?`)) return;
<div class="modal-header"> const r = await fetch(`/api/user/${userId}/verify`, { method: 'POST' });
<h5 class="modal-title">Редактировать пользователя</h5> const result = await r.json();
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> if (result.success) { alert('✅ Пользователь верифицирован!'); location.reload(); }
</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> async function quickReject(reqId) {
<script> if (!confirm('Отклонить заявку на верификацию?')) return;
// Быстрая верификация const r = await fetch(`/api/verification/${reqId}/reject`, { method: 'POST' });
async function quickVerify(userId, apartment) { const result = await r.json();
if (!confirm(`Верифицировать пользователя (кв. ${apartment})?`)) return; if (result.success) { alert('✅ Заявка отклонена'); location.reload(); }
}
try { const editModal = new bootstrap.Modal(document.getElementById('editUserModal'));
const r = await fetch(`/api/user/${userId}/verify`, { method: 'POST' }); let currentUserId = null;
const result = await r.json();
if (result.success) {
alert('✅ Пользователь верифицирован!');
location.reload();
}
} catch (err) {
alert('❌ Ошибка: ' + err.message);
}
}
// Быстрое отклонение function editUser(userId) {
async function quickReject(reqId) { currentUserId = userId;
if (!confirm('Отклонить заявку на верификацию?')) return; 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();
});
}
try { async function saveUser() {
const r = await fetch(`/api/verification/${reqId}/reject`, { method: 'POST' }); const userId = document.getElementById('editUserId').value;
const result = await r.json(); const data = {
if (result.success) { apartment: document.getElementById('editApartment').value,
alert('✅ Заявка отклонена'); phone: document.getElementById('editPhone').value,
location.reload(); rating: parseInt(document.getElementById('editRating').value)
} };
} catch (err) { const response = await fetch(`/api/user/${userId}/update`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
alert('❌ Ошибка: ' + err.message); const result = await response.json();
} if (result.success) { alert('✅ Данные обновлены'); location.reload(); }
} else { alert('❌ Ошибка: ' + result.error); }
}
const editModal = new bootstrap.Modal(document.getElementById('editUserModal')); 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(); } }
let currentUserId = null; 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(); } }
function editUser(userId) { 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(); } }
currentUserId = userId; 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(); } }
fetch(`/api/user/${userId}`) 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(); } }
.then(r => r.json()) 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(); } }
.then(data => { 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(); } }
const user = data.user; </script>
document.getElementById('editUserId').value = user.id; {% endblock %}
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>

View file

@ -1,190 +1,161 @@
<!DOCTYPE html> {% extends "base.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; }
.status-pending { color: #f39c12; }
.status-approved { color: #27ae60; }
.status-rejected { color: #c0392b; }
</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" href="/users"><i class="bi bi-people"></i> Пользователи</a>
<a class="nav-link active" 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="/scheduled_posts"><i class="bi bi-calendar2-plus"></i> Запланированные посты</a>
</nav>
</div>
<div class="col-md-10 content"> {% block title %}Верификация - Домовой Бот{% endblock %}
<h2 class="mb-4"><i class="bi bi-shield-check"></i> Управление верификацией</h2>
<!-- Заявки на верификацию --> {% block nav_verification %}active{% endblock %}
<div class="card mb-4">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Заявки на проверку</h5>
</div>
<div class="card-body">
{% if pending_requests %}
<table class="table table-hover">
<thead>
<tr>
<th>ID заявки</th>
<th>User ID</th>
<th>Имя</th>
<th>Квартира</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for req in pending_requests %}
<tr>
<td>{{ req.id }}</td>
<td>{{ req.user_id }}</td>
<td>{{ req.comment or '—' }}</td>
<td>{{ req.apartment or '—' }}</td>
<td>{{ req.created_at.strftime('%d.%m.%Y %H:%M') if req.created_at else '—' }}</td>
<td>
<button class="btn btn-sm btn-success" onclick="approveVerification({{ req.id }})">
<i class="bi bi-check-lg"></i> Одобрить
</button>
<button class="btn btn-sm btn-danger" onclick="rejectVerification({{ req.id }})">
<i class="bi bi-x-lg"></i> Отклонить
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted text-center">Нет заявок на верификацию</p>
{% endif %}
</div>
</div>
<!-- Непроверенные пользователи --> {% block extra_style %}
<div class="card"> .status-pending { color: #f39c12; }
<div class="card-header bg-secondary text-white"> .status-approved { color: #27ae60; }
<h5 class="mb-0"><i class="bi bi-person-x"></i> Непроверенные пользователи</h5> .status-rejected { color: #c0392b; }
</div> {% endblock %}
<div class="card-body">
{% if unverified_users %} {% block content %}
<table class="table table-hover"> <h2 class="mb-4"><i class="bi bi-shield-check"></i> Управление верификацией</h2>
<thead>
<tr> <!-- Заявки на верификацию -->
<th>User ID</th> <div class="card mb-4">
<th>Имя</th> <div class="card-header bg-warning text-dark">
<th>Квартира</th> <h5 class="mb-0"><i class="bi bi-clock-history"></i> Заявки на проверку</h5>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in unverified_users %}
<tr>
<td>{{ user.user_id }}</td>
<td>{{ user.full_name or '—' }}</td>
<td>{{ user.apartment or '—' }}</td>
<td>{{ user.join_date.strftime('%d.%m.%Y') if user.join_date else '—' }}</td>
<td>
<button class="btn btn-sm btn-success" onclick="verifyUser({{ user.user_id }})">
<i class="bi bi-check-lg"></i> Верифицировать
</button>
<button class="btn btn-sm btn-warning" onclick="unverifyUser({{ user.user_id }})">
<i class="bi bi-x-lg"></i> Отозвать
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted text-center">Все пользователи верифицированы</p>
{% endif %}
</div>
</div>
</div>
</div>
</div> </div>
<div class="card-body">
{% if pending_requests %}
<table class="table table-hover">
<thead>
<tr>
<th>ID заявки</th>
<th>User ID</th>
<th>Имя</th>
<th>Квартира</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for req in pending_requests %}
<tr>
<td>{{ req.id }}</td>
<td>{{ req.user_id }}</td>
<td>{{ req.comment or '—' }}</td>
<td>{{ req.apartment or '—' }}</td>
<td>{{ req.created_at.strftime('%d.%m.%Y %H:%M') if req.created_at else '—' }}</td>
<td>
<button class="btn btn-sm btn-success" onclick="approveVerification({{ req.id }})">
<i class="bi bi-check-lg"></i> Одобрить
</button>
<button class="btn btn-sm btn-danger" onclick="rejectVerification({{ req.id }})">
<i class="bi bi-x-lg"></i> Отклонить
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted text-center">Нет заявок на верификацию</p>
{% endif %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Непроверенные пользователи -->
<script> <div class="card">
async function approveVerification(requestId) { <div class="card-header bg-secondary text-white">
if (!confirm('Одобрить верификацию?')) return; <h5 class="mb-0"><i class="bi bi-person-x"></i> Непроверенные пользователи</h5>
</div>
<div class="card-body">
{% if unverified_users %}
<table class="table table-hover">
<thead>
<tr>
<th>User ID</th>
<th>Имя</th>
<th>Квартира</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in unverified_users %}
<tr>
<td>{{ user.user_id }}</td>
<td>{{ user.full_name or '—' }}</td>
<td>{{ user.apartment or '—' }}</td>
<td>{{ user.join_date.strftime('%d.%m.%Y') if user.join_date else '—' }}</td>
<td>
<button class="btn btn-sm btn-success" onclick="verifyUser({{ user.user_id }})">
<i class="bi bi-check-lg"></i> Верифицировать
</button>
<button class="btn btn-sm btn-warning" onclick="unverifyUser({{ user.user_id }})">
<i class="bi bi-x-lg"></i> Отозвать
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted text-center">Все пользователи верифицированы</p>
{% endif %}
</div>
</div>
{% endblock %}
const response = await fetch(`/api/verification/${requestId}/approve`, { method: 'POST' }); {% block extra_script %}
const result = await response.json(); <script>
async function approveVerification(requestId) {
if (!confirm('Одобрить верификацию?')) return;
if (result.success) { const response = await fetch(`/api/verification/${requestId}/approve`, { method: 'POST' });
alert('✅ Верификация одобрена'); const result = await response.json();
location.reload();
} else { if (result.success) {
alert('❌ Ошибка: ' + result.error); alert('✅ Верификация одобрена');
} location.reload();
} else {
alert('❌ Ошибка: ' + result.error);
} }
}
async function rejectVerification(requestId) { async function rejectVerification(requestId) {
if (!confirm('Отклонить верификацию?')) return; if (!confirm('Отклонить верификацию?')) return;
const response = await fetch(`/api/verification/${requestId}/reject`, { method: 'POST' }); const response = await fetch(`/api/verification/${requestId}/reject`, { method: 'POST' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
alert('Верификация отклонена'); alert('Верификация отклонена');
location.reload(); location.reload();
} else { } else {
alert('❌ Ошибка: ' + result.error); alert('❌ Ошибка: ' + result.error);
}
} }
}
async function verifyUser(userId) { async function verifyUser(userId) {
if (!confirm('Верифицировать пользователя?')) return; if (!confirm('Верифицировать пользователя?')) return;
const response = await fetch(`/api/user/${userId}/verify`, { method: 'POST' }); const response = await fetch(`/api/user/${userId}/verify`, { method: 'POST' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
alert('✅ Пользователь верифицирован'); alert('✅ Пользователь верифицирован');
location.reload(); location.reload();
} else { } else {
alert('❌ Ошибка: ' + result.error); alert('❌ Ошибка: ' + result.error);
}
} }
}
async function unverifyUser(userId) { async function unverifyUser(userId) {
if (!confirm('Отозвать верификацию у пользователя?')) return; if (!confirm('Отозвать верификацию у пользователя?')) return;
const response = await fetch(`/api/user/${userId}/unverify`, { method: 'POST' }); const response = await fetch(`/api/user/${userId}/unverify`, { method: 'POST' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
alert('⚠️ Верификация отозвана'); alert('⚠️ Верификация отозвана');
location.reload(); location.reload();
} else { } else {
alert('❌ Ошибка: ' + result.error); alert('❌ Ошибка: ' + result.error);
}
} }
</script> }
</body> </script>
</html> {% endblock %}