domovoy_bot/web/templates/scheduled_posts.html
Admin 54e8cea522 📄 feat: Отправка документов (PDF, DOCX, XLSX...) через веб-панель
Новый функционал:
- Загрузка документов через веб-панель (10 форматов)
- Отправка в любую тему форума по расписанию
- Поддержка: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, ZIP, RAR
- Отправка в чат и/или всем верифицированным жильцам
- UI: превью документа, кнопка удаления

Изменения:
- database/models.py: document_file_id, document_name поля
- database/migrate_v3_4_documents.py: миграция БД
- web/app.py: API endpoint с загрузкой документов
- services/scheduler.py: отправка документов по расписанию
- web/templates/scheduled_posts.html: UI для загрузки документов

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-13 21:51:52 +04:00

305 lines
14 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.

{% extends "base.html" %}
{% block title %}Запланированные посты - Домовой Бот{% endblock %}
{% block nav_scheduled_posts %}active{% endblock %}
{% block extra_style %}
#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); }
{% endblock %}
{% block 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="mb-3">
<label class="form-label">📄 Документ (необязательно)</label>
<input type="file" class="form-control" id="postDocument" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
onchange="previewDocument()">
<small class="text-muted">PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT, ZIP, RAR до 50MB</small>
<div id="documentPreview" class="mt-2" style="display:none;">
<span class="badge bg-success">📄 <span id="documentName"></span></span>
<button type="button" class="btn btn-sm btn-danger ms-2" onclick="clearDocument()">✕ Удалить</button>
</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>
{% endblock %}
{% block extra_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';
}
}
function previewDocument() {
const file = document.getElementById('postDocument').files[0];
const preview = document.getElementById('documentPreview');
const nameSpan = document.getElementById('documentName');
if (file) {
nameSpan.textContent = file.name;
preview.style.display = 'block';
// Очищаем фото если выбран документ
document.getElementById('postPhoto').value = '';
document.getElementById('preview').style.display = 'none';
} else {
preview.style.display = 'none';
}
}
function clearDocument() {
document.getElementById('postDocument').value = '';
document.getElementById('documentPreview').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);
}
const doc = document.getElementById('postDocument').files[0];
if (doc) {
formData.append('document', doc);
}
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>
{% endblock %}