domovoy_bot/web/templates/scheduled_posts.html
Admin ccc816ce4f 🕐 fix: локальное время UTC+4 + защита от дублирования постов
- Timezone: веб-панель показывает локальное время (UTC+4, Ульяновск)
- Timezone: конвертация UTC → локальное при отображении
- Duplicate fix: промежуточный статус 'sending' в БД
- Duplicate fix: только один процесс берет пост
- UI: подсказки 'По твоему времени (UTC+4)'
- Тест: пост 5 опубликован ровно 1 раз 

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-05 22:37:35 +04:00

296 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<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; }
#preview { max-width: 400px; margin-top: 1rem; display: none; }
#preview img { max-width: 100%; border-radius: 8px; }
.post-card { transition: transform 0.2s; }
.post-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
</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="/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 class="col-md-10 content">
<h2 class="mb-4"><i class="bi bi-calendar2-plus"></i> Запланированные посты</h2>
<!-- Форма создания поста -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-plus-circle"></i> Создать новый пост</h5>
</div>
<div class="card-body">
<form id="scheduledPostForm" enctype="multipart/form-data">
<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>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Установка минимальной даты (сегодня)
document.addEventListener('DOMContentLoaded', function() {
const today = new Date();
const todayStr = today.toISOString().split('T')[0];
document.getElementById('postDate').setAttribute('min', todayStr);
// Показываем текущее время в браузере пользователя
const tzOffset = -today.getTimezoneOffset() / 60;
const tzLabel = tzOffset >= 0 ? `UTC+${tzOffset}` : `UTC${tzOffset}`;
console.log(`🕐 Твой часовой пояс: ${tzLabel}`);
loadPosts();
});
function previewImage() {
const file = document.getElementById('postPhoto').files[0];
const preview = document.getElementById('preview');
const previewImg = document.getElementById('previewImg');
if (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 {
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>
`;
postsList.appendChild(card);
});
} else {
noPosts.style.display = 'block';
postsList.innerHTML = '';
}
} catch (err) {
console.error('Ошибка загрузки постов:', err);
}
}
// Отмена поста
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>
</body>
</html>