Files
Zilant2025/templates/index.html
T
2025-09-28 20:33:35 +03:00

2609 lines
128 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">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление постами VK и событиями</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Table styling fixes */
.table-header-container table,
.table-header-container th,
.table-header-container td,
.scrollable-table-body table,
.scrollable-table-body th,
.scrollable-table-body td {
box-sizing: border-box;
}
.scrollable-table-body {
overflow-x: auto;
}
.post-text-preview {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.shortname-preview {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.event-name-preview {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.detail-image {
max-height: 400px;
object-fit: contain;
}
.card-header {
background-color: #f8f9fa;
font-weight: 600;
}
.action-buttons .btn {
margin-right: 5px;
}
/* Отступ для колонки действий от скроллбара */
.action-buttons {
padding-right: 20px !important;
min-width: 120px;
}
/* Отступ для заголовка колонки действий */
th.text-end:last-child {
padding-right: 20px !important;
min-width: 120px;
}
.post-row, .event-row {
cursor: pointer;
transition: all 0.2s ease;
}
.post-row:hover, .event-row:hover {
background-color: #f8f9fa;
transition: all 0.2s ease;
}
.post-row:hover:not(.active), .event-row:hover:not(.active) {
background-color: #f1f1f1;
}
.post-row.active, .event-row.active {
background-color: #e3f2fd !important;
background-image: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%) !important;
outline: 2px solid #0d6efd;
outline-offset: 0px;
box-shadow: 0 2px 4px rgba(13, 110, 253, 0.15);
}
.post-row.active td, .event-row.active td {
background-color: transparent !important;
}
.status-badge {
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
}
/* Улучшенная видимость бейджей на выбранных строках */
.post-row.active .status-badge, .event-row.active .status-badge {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
}
/* Улучшенная видимость иконок на выбранных строках */
.post-row.active .btn, .event-row.active .btn {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Улучшенная контрастность текста на выбранных строках */
.post-row.active td, .event-row.active td {
color: #1a1a1a;
font-weight: 600;
}
/* Специальный стиль для превью текста на выбранных строках */
.post-row.active .post-text-preview,
.post-row.active .shortname-preview,
.event-row.active .event-name-preview {
color: #2c3e50;
font-weight: 600;
}
/* Обеспечиваем одинаковую высоту для всех карточек в панелях */
.row .card {
height: 100%;
display: flex;
flex-direction: column;
}
/* Карточка детализации занимает всю высоту родительского контейнера */
.card.h-100 {
height: 100%;
display: flex;
flex-direction: column;
}
.card.h-100 .card-body {
flex: 0 0 auto;
display: block;
padding: 1rem;
overflow: visible;
min-height: auto;
}
/* Скроллируемая область детализации */
.scrollable-details {
flex: 0 0 auto;
overflow: visible !important;
padding: 1rem !important;
margin: 0 !important;
position: relative;
min-height: auto;
height: auto;
max-height: none;
}
/* Контент внутри детализации */
.scrollable-details > div {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
overflow: visible !important;
box-sizing: border-box;
width: 100%;
height: auto;
}
.navbar-nav .dropdown-menu {
min-width: 200px;
}
.top-action-buttons {
margin-bottom: 15px;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.top-action-buttons .btn {
margin-right: 8px;
}
/* Убираем лишние отступы на вкладке событий */
#events-tab-pane .top-action-buttons {
margin-bottom: 10px; /* Уменьшаем отступ */
}
/* Убираем отступы у скрытых элементов */
#event-selected-post-info {
margin: 0;
padding: 0;
}
/* Добавляем отступ снизу для вкладки событий */
#events-tab-pane {
padding-bottom: 10px;
box-sizing: border-box;
max-height: calc(100vh - 150px);
}
#processing-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1050;
display: none;
}
.nav-tabs .nav-link {
font-weight: 500;
}
.tab-content {
padding-top: 15px;
}
.event-details-row {
margin-bottom: 10px;
}
.event-details-label {
font-weight: bold;
margin-bottom: 5px;
}
.event-details-value {
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
min-height: 38px;
}
.cross-tab-info {
background-color: #f8f9fa;
border-radius: 4px;
padding: 8px 12px;
margin-left: 15px;
display: inline-flex;
align-items: center;
}
.cross-tab-info span {
margin-right: 8px;
}
.linked-items-list {
max-height: 200px;
overflow-y: auto;
}
/* Добавляем стили для сортируемых заголовков и разделителей */
.sortable-header {
cursor: pointer;
position: relative;
user-select: none;
}
.sortable-header:hover {
background-color: #e9ecef;
}
.sortable-header::after {
content: '';
display: inline-block;
margin-left: 5px;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
opacity: 0.5;
}
.sortable-header.asc::after {
border-bottom: 5px solid #000;
}
.sortable-header.desc::after {
border-top: 5px solid #000;
}
/* Стили для разделителей колонок */
.column-resizer {
position: absolute;
top: 0;
right: 0;
width: 5px;
height: 100%;
cursor: col-resize;
user-select: none;
}
/* Стили для разделителя панелей */
.panel-resizer {
width: 10px;
background-color: #f0f0f0;
cursor: col-resize;
position: relative;
z-index: 10;
}
.panel-resizer:hover {
background-color: #d0d0d0;
}
/* Убираем стандартное поведение Bootstrap колонок */
.resizable-panel {
transition: none;
overflow: auto;
}
/* Гарантируем, что панели будут в одной строке */
.resizable-row {
display: flex;
flex-wrap: nowrap;
width: 100%;
}
/* Table structure */
.table-container {
position: relative;
}
.table-header-container {
background-color: #f8f9fa;
position: sticky;
top: 0;
z-index: 10;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
}
.scrollable-table-body {
max-height: calc(100vh - 290px);
overflow-y: auto;
overflow-x: hidden;
position: relative;
}
.table-header-container th,
.scrollable-table-body td {
vertical-align: top;
box-sizing: border-box;
}
.table-header-container,
.scrollable-table-body {
width: 100%;
}
.table-header-container table,
.scrollable-table-body table {
table-layout: fixed;
width: 100%;
margin: 0;
border-collapse: separate;
border-spacing: 0;
}
.table-header-container .table,
.scrollable-table-body .table {
margin-bottom: 0;
}
/* Добавляем тень для визуального отделения скроллируемой области */
.scrollable-table-body {
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Управление постами VK и событиями</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user me-1"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" href="{{ url_for('zilant.change_password') }}">
<i class="fas fa-key me-2"></i> Сменить пароль
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="#" id="reset-layout-btn">
<i class="fas fa-undo me-2"></i> Сбросить настройки интерфейса
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="{{ url_for('zilant.logout') }}">
<i class="fas fa-sign-out-alt me-2"></i> Выйти
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<!-- Навигационные вкладки -->
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="posts-tab" data-bs-toggle="tab" data-bs-target="#posts-tab-pane" type="button" role="tab" aria-controls="posts-tab-pane" aria-selected="true">
<i class="fas fa-newspaper me-1"></i> Посты
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events-tab-pane" type="button" role="tab" aria-controls="events-tab-pane" aria-selected="false">
<i class="fas fa-calendar-alt me-1"></i> События
</button>
</li>
</ul>
<!-- Содержимое вкладок -->
<div class="tab-content" id="mainTabsContent">
<!-- Вкладка постов -->
<div class="tab-pane fade show active" id="posts-tab-pane" role="tabpanel" aria-labelledby="posts-tab" tabindex="0">
<!-- Кнопки запуска скриптов -->
<div class="top-action-buttons d-flex align-items-center">
<button class="btn btn-primary" id="rescan-btn">
<i class="fas fa-search me-1"></i> Рескан ВК
</button>
<button class="btn btn-success" id="publish-btn">
<i class="fas fa-paper-plane me-1"></i> Опубликовать
</button>
<button class="btn btn-info" id="generate-names-btn">
<i class="fas fa-font me-1"></i> Названия
</button>
<div id="post-selected-event-info" class="cross-tab-info" style="display: none;">
<span>Выбранное событие: </span>
<span id="post-selected-event-name"></span>
<button class="btn btn-sm btn-outline-primary ms-2" id="post-link-event-btn">Привязать</button>
</div>
</div>
<div class="row">
<!-- Левая панель - список постов -->
<div class="col-md-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Список постов</h5>
<div>
<button class="btn btn-sm btn-success" id="add-post-btn">
<i class="fas fa-plus"></i> Добавить
</button>
<button class="btn btn-sm btn-primary" id="refresh-posts-btn">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<!-- Контейнер для фиксированного заголовка -->
<div class="table-header-container">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="sortable-header">ID</th>
<th class="sortable-header">VK ID</th>
<th class="sortable-header">Название</th>
<th id="post-text-column-header" style="display: none;" class="sortable-header">Текст</th>
<th class="sortable-header">Статус</th>
<th class="text-end">Действия</th>
</tr>
</thead>
</table>
</div>
<!-- Контейнер для скроллируемого тела таблицы -->
<div class="scrollable-table-body">
<table class="table table-hover mb-0">
<tbody id="posts-table">
<tr>
<td colspan="6" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Правая панель - детали поста -->
<div class="col-md-7">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="post-details-header">Детали поста</h5>
<div id="post-actions"></div>
</div>
<div class="card-body scrollable-details" id="post-details">
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<p>Выберите пост для просмотра деталей</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Вкладка событий -->
<div class="tab-pane fade" id="events-tab-pane" role="tabpanel" aria-labelledby="events-tab" tabindex="0">
<div class="top-action-buttons d-flex align-items-center">
<button class="btn btn-success" id="add-event-btn">
<i class="fas fa-plus me-1"></i> Добавить событие
</button>
<button class="btn btn-primary" id="zk-rescan-btn">
<i class="fas fa-search me-1"></i> Рескан Зилант-сервера
</button>
<button class="btn btn-success" id="publish-all-events-btn">
<i class="fas fa-paper-plane me-1"></i> Опубликовать все
</button>
<div id="event-selected-post-info" class="cross-tab-info" style="display: none;">
<span>Выбранный пост: </span>
<span id="event-selected-post-name"></span>
<button class="btn btn-sm btn-outline-primary ms-2" id="event-link-post-btn">Привязать</button>
</div>
</div>
<div class="row">
<!-- Левая панель - список событий -->
<div class="col-md-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Список событий</h5>
<div>
<button class="btn btn-sm btn-success" id="add-event-btn-sm">
<i class="fas fa-plus"></i> Добавить
</button>
<button class="btn btn-sm btn-primary" id="refresh-events-btn">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<!-- Контейнер для фиксированного заголовка -->
<div class="table-header-container">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="sortable-header">ID</th>
<th class="sortable-header">Номер</th>
<th class="sortable-header">Название</th>
<th class="sortable-header">Площадка</th>
<th class="sortable-header">Добавлено</th>
<th class="sortable-header">Обновлено</th>
<th class="sortable-header">Статус</th>
<th class="text-end">Действия</th>
</tr>
</thead>
</table>
</div>
<!-- Контейнер для скроллируемого тела таблицы -->
<div class="scrollable-table-body">
<table class="table table-hover mb-0">
<tbody id="events-table">
<tr>
<td colspan="8" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Правая панель - детали события -->
<div class="col-md-7">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Детали события</h5>
<div id="event-actions"></div>
</div>
<div class="card-body scrollable-details" id="event-details">
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<p>Выберите событие для просмотра деталей</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения удаления поста -->
<div class="modal fade" id="deleteModal" 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">
<p>Вы уверены, что хотите удалить этот пост? Это действие нельзя отменить.</p>
<input type="hidden" id="delete-post-id">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" id="confirm-delete">Удалить</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно подтверждения удаления события -->
<div class="modal fade" id="deleteEventModal" 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">
<p>Вы уверены, что хотите удалить это событие? Это действие нельзя отменить.</p>
<input type="hidden" id="delete-event-id">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-danger" id="confirm-event-delete">Удалить</button>
</div>
</div>
</div>
</div>
<!-- Сообщение о процессе выполнения -->
<div id="processing-message" class="alert alert-info">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm me-2" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<span id="processing-text">Обработка...</span>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const PREFIX = "{{ g.prefix }}";
console.log("Lister Prefix is:", PREFIX); // Должно выводить "/zilant"
// Текущий выбранный пост и событие
let selectedPostId = null;
let selectedEventId = null;
// Данные о выбранных элементах для отображения на других вкладках
let selectedPostData = null;
let selectedEventData = null;
// Загрузка постов при запуске
document.addEventListener('DOMContentLoaded', () => {
loadPosts();
// Добавляем обработчики для кнопок запуска скриптов
document.getElementById('rescan-btn').addEventListener('click', runRescanScript);
document.getElementById('publish-btn').addEventListener('click', runPublishScript);
document.getElementById('generate-names-btn').addEventListener('click', generateAllShortnames);
document.getElementById('refresh-events-btn').addEventListener('click', loadEvents);
// Обработчики для кнопок добавления события
document.getElementById('add-event-btn').addEventListener('click', () => {
window.location.href = `${PREFIX}/add_event`;
});
document.getElementById('add-event-btn-sm').addEventListener('click', () => {
window.location.href = `${PREFIX}/add_event`;
});
// Подтверждение удаления события
document.getElementById('confirm-event-delete').addEventListener('click', () => {
const eventId = document.getElementById('delete-event-id').value;
deleteEvent(eventId);
});
// Обработчики для вкладок
document.getElementById('posts-tab').addEventListener('click', () => {
if (document.getElementById('posts-table').children.length === 1) {
loadPosts();
updateSelectedEventInfo();
}
// Принудительно обновляем размеры панелей и синхронизируем колонки
setTimeout(() => {
adjustPanelWidthsOnResize();
syncColumnWidths('posts');
}, 50);
});
document.getElementById('events-tab').addEventListener('click', () => {
if (document.getElementById('events-table').children.length === 1) {
loadEvents();
updateSelectedPostInfo();
}
// Принудительно обновляем размеры панелей и синхронизируем колонки
setTimeout(() => {
adjustPanelWidthsOnResize();
syncColumnWidths('events');
}, 50);
});
// Новые обработчики для кнопок событий
document.getElementById('zk-rescan-btn').addEventListener('click', runZkRescan);
document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents);
// Обработчики для кнопок привязки
document.getElementById('post-link-event-btn').addEventListener('click', togglePostEventLink);
document.getElementById('event-link-post-btn').addEventListener('click', togglePostEventLink);
// Обработка параметров URL для активации вкладок
const urlParams = new URLSearchParams(window.location.search);
const selectedPostId = urlParams.get('selected_post');
const selectedEventId = urlParams.get('selected_event');
const tabParam = urlParams.get('tab');
// Если есть selectedEventId или параметр tab=events, активируем вкладку событий
if (selectedEventId || tabParam === 'events') {
const eventsTab = document.getElementById('events-tab');
if (eventsTab) {
// Активируем вкладку событий
const tabInstance = new bootstrap.Tab(eventsTab);
tabInstance.show();
// Также загружаем события, если они еще не загружены
if (document.getElementById('events-table').children.length === 1) {
loadEvents();
}
}
}
// Если есть selectedPostId, активируем вкладку постов (по умолчанию и так активна, но если есть selectedEventId, то переключится на события)
if (selectedPostId) {
// Убедимся, что вкладка постов активна, если нет selectedEventId
if (!selectedEventId && tabParam !== 'events') {
const postsTab = document.getElementById('posts-tab');
if (postsTab) {
const tabInstance = new bootstrap.Tab(postsTab);
tabInstance.show();
}
}
}
// Затем выделяем элементы
if (selectedPostId) {
highlightSelectedPost(selectedPostId);
showPostDetails(selectedPostId);
}
if (selectedEventId) {
highlightSelectedEvent(selectedEventId);
showEventDetails(selectedEventId);
}
});
// Кнопка обновления постов
document.getElementById('refresh-posts-btn').addEventListener('click', loadPosts);
// Кнопка добавления поста
document.getElementById('add-post-btn').addEventListener('click', () => {
window.location.href = `${PREFIX}/add_post`;
});
// Подтверждение удаления поста
document.getElementById('confirm-delete').addEventListener('click', () => {
const postId = document.getElementById('delete-post-id').value;
deletePost(postId);
});
// Запуск скрипта рескана ВК
function runRescanScript() {
runScript('rescan');
}
// Запуск скрипта публикации
function runPublishScript() {
runScript('publish');
}
// Рескан Зилант-сервера
function runZkRescan() {
showProcessingMessage("Рескан Зилант-сервера...");
fetch(`${PREFIX}/api/run_zk_rescan`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Рескан Зилант-сервера выполнен успешно!');
loadEvents();
} else {
alert('Ошибка рескана Зилант-сервера: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка рескана Зилант-сервера:', error);
alert('Произошла ошибка при рескане Зилант-сервера');
});
}
// Функция для публикации всех событий
function publishAllEvents() {
const currentEventId = selectedEventId;
showProcessingMessage("Публикация всех событий...");
// Используем тот же подход, что и для других асинхронных операций
fetch(`${PREFIX}/api/publish_all_events`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Публикация всех событий выполнена успешно!');
loadEvents().then(() => {
if (currentEventId) {
highlightSelectedEvent(currentEventId);
showEventDetails(currentEventId);
}
});
} else {
alert('Ошибка публикации всех событий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка публикации всех событий:', error);
alert('Произошла ошибка при публикации всех событий');
});
}
// Функция для публикации конкретного события
function publishCurrentEvent(eventId) {
if (!eventId) return;
const currentEventId = eventId;
showProcessingMessage("Публикация события...");
// Используем тот же подход, что и для других асинхронных операций
fetch(`${PREFIX}/api/publish_event/${currentEventId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Событие опубликовано успешно!');
loadEvents().then(() => {
highlightSelectedEvent(currentEventId);
showEventDetails(currentEventId);
});
} else {
alert('Ошибка публикации события: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка публикации события:', error);
alert('Произошла ошибка при публикации события');
});
}
// Привязка/отвязка поста и события
// Замените текущую функцию togglePostEventLink на эту обновленную версию
function togglePostEventLink() {
// Определяем, на какой вкладке мы находимся и какие элементы выбраны
const isOnPostsTab = document.getElementById('posts-tab-pane').classList.contains('show');
const isOnEventsTab = document.getElementById('events-tab-pane').classList.contains('show');
let postId, eventId;
if (isOnPostsTab) {
// Мы на вкладке постов - используем выбранный пост и выбранное событие из другой вкладки
postId = selectedPostId;
eventId = selectedEventData ? selectedEventData.id : null;
} else if (isOnEventsTab) {
// Мы на вкладке событий - используем выбранный пост из другой вкладки и выбранное событие
postId = selectedPostData ? selectedPostData.id : null;
eventId = selectedEventId;
}
if (!postId || !eventId) {
alert('Не выбран пост или событие для привязки');
return;
}
// Проверяем текущее состояние связи
fetch(`${PREFIX}/api/interlinks?post_id=${postId}&event_id=${eventId}`)
.then(response => response.json())
.then(data => {
if (data.exists) {
// Удаляем связь
return fetch(`${PREFIX}/api/interlinks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ post_id: postId, event_id: eventId })
});
} else {
// Создаем связь
return fetch(`${PREFIX}/api/interlinks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ post_id: postId, event_id: eventId })
});
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Обновляем информацию о связях
updateInterlinkInfo();
// Обновляем детали выбранного элемента
if (selectedPostId) showPostDetails(selectedPostId);
if (selectedEventId) showEventDetails(selectedEventId);
alert('Связь успешно ' + (data.exists ? 'удалена' : 'установлена'));
} else {
alert('Ошибка при изменении связи: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
console.error('Ошибка при изменении связи:', error);
alert('Произошла ошибка при изменении связи');
});
}
// Обновите функцию updateInterlinkInfo для правильной работы с новыми переменными
function updateInterlinkInfo() {
// Для вкладки постов
if (selectedPostId && selectedEventData) {
fetch(`${PREFIX}/api/interlinks?post_id=${selectedPostId}&event_id=${selectedEventData.id}`)
.then(response => response.json())
.then(data => {
const btn = document.getElementById('post-link-event-btn');
if (btn) {
if (data.exists) {
btn.textContent = 'Отвязать';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-outline-danger');
} else {
btn.textContent = 'Привязать';
btn.classList.remove('btn-outline-danger');
btn.classList.add('btn-outline-primary');
}
}
})
.catch(error => {
console.error('Ошибка проверки связи:', error);
});
}
// Для вкладки событий
if (selectedEventId && selectedPostData) {
fetch(`${PREFIX}/api/interlinks?post_id=${selectedPostData.id}&event_id=${selectedEventId}`)
.then(response => response.json())
.then(data => {
const btn = document.getElementById('event-link-post-btn');
if (btn) {
if (data.exists) {
btn.textContent = 'Отвязать';
btn.classList.remove('btn-outline-primary');
btn.classList.add('btn-outline-danger');
} else {
btn.textContent = 'Привязать';
btn.classList.remove('btn-outline-danger');
btn.classList.add('btn-outline-primary');
}
}
})
.catch(error => {
console.error('Ошибка проверки связи:', error);
});
}
}
// Общая функция запуска скриптов
function runScript(scriptType) {
showProcessingMessage("Запуск скрипта...");
fetch(`${PREFIX}/api/run_script`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ script: scriptType })
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Скрипт успешно запущен!');
// Обновляем список постов после выполнения скрипта
loadPosts();
} else {
alert('Ошибка запуска скрипта: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка запуска скрипта:', error);
alert('Произошла ошибка при запуске скрипта');
});
}
// Функция для генерации всех названий
function generateAllShortnames() {
showProcessingMessage("Генерация названий для всех постов...");
fetch(`${PREFIX}/api/generate_all_shortnames`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Названия успешно сгенерированы!');
loadPosts(); // Перезагружаем список постов
} else {
alert('Ошибка генерации названий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка генерации названий:', error);
alert('Произошла ошибка при генерации названий');
});
}
// Функция для генерации названия для конкретного поста
function generateShortname(postId) {
showProcessingMessage("Генерация названия для поста...");
fetch(`${PREFIX}/api/generate_shortname/${postId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Название успешно сгенерировано: ' + data.shortname);
loadPosts(); // Перезагружаем список постов
// Восстанавливаем выбранный пост
highlightSelectedPost(postId);
showPostDetails(postId);
} else {
alert('Ошибка генерации названия: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка генерации названия:', error);
alert('Произошла ошибка при генерации названия');
});
}
// Функция для публикации конкретного поста
function publishPost(postId) {
showProcessingMessage("Публикация поста...");
fetch(`${PREFIX}/api/publish_post/${postId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Пост успешно опубликован!');
loadPosts(); // Перезагружаем список постов
// Восстанавливаем выбранный пост
highlightSelectedPost(postId);
showPostDetails(postId);
} else {
alert('Ошибка публикации поста: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка публикации поста:', error);
alert('Произошла ошибка при публикации поста');
});
}
// Функция для переключения статуса публикации события
async function toggleEventPublication(eventId) {
showProcessingMessage("Изменение статуса публикации...");
fetch(`${PREFIX}/api/events/${eventId}/toggle_publication`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
// Обновляем список событий
loadEvents();
// Обновляем детали события
showEventDetails(eventId);
} else {
alert('Ошибка изменения статуса публикации: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка изменения статуса публикации:', error);
alert('Произошла ошибка при изменении статуса публикации');
});
}
// Функция для удаления события
async function deleteEvent(eventId) {
const deleteModal = bootstrap.Modal.getInstance('#deleteEventModal');
try {
const response = await fetch(`${PREFIX}/api/events/${eventId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
deleteModal.hide();
loadEvents();
// Сбрасываем детали события
document.getElementById('event-details').innerHTML = `
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<p>Выберите событие для просмотра деталей</p>
</div>`;
} else {
throw new Error('Ошибка при удалении события');
}
} catch (error) {
alert(`Ошибка удаления: ${error.message}`);
}
}
// Функции для показа/скрытия сообщения о обработке
function showProcessingMessage(message) {
const processingElement = document.getElementById('processing-message');
const processingText = document.getElementById('processing-text');
processingText.textContent = message;
processingElement.style.display = 'flex';
}
function hideProcessingMessage() {
const processingElement = document.getElementById('processing-message');
processingElement.style.display = 'none';
}
// Функция загрузки событий
async function loadEvents() {
const tableBody = document.getElementById('events-table');
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
Загрузка событий...
</td>
</tr>`;
try {
const response = await fetch(`${PREFIX}/api/events`);
const events = await response.json();
if (events.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p>Нет доступных событий</p>
</td>
</tr>`;
return;
}
tableBody.innerHTML = '';
events.forEach(event => {
// Форматируем даты
const formatDateTime = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
const visibleBadge = event.is_visible
? '<span class="badge bg-success status-badge">Видно</span>'
: '<span class="badge bg-secondary status-badge">Не видно</span>';
const publicationBadge = event.marked_to_publication
? '<span class="badge bg-warning status-badge ms-1">Для публикации</span>'
: '';
const postedBadge = event.is_posted_tg
? '<span class="badge bg-primary status-badge ms-1">Опубликовано</span>'
: '';
const row = `
<tr class="event-row" data-id="${event.id}">
<td>${event.id}</td>
<td>${event.number}</td>
<td class="event-name-preview" title="${event.name || ''}">${event.name || '-'}</td>
<td class="shortname-preview" title="${event.unit_name || ''}">${event.unit_name || '-'}</td>
<td>${formatDateTime(event.added)}</td>
<td>${formatDateTime(event.modified)}</td>
<td>${visibleBadge}${publicationBadge}${postedBadge}</td>
<td class="text-end action-buttons">
<button class="btn btn-sm btn-outline-warning edit-event-btn">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-event-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`;
tableBody.innerHTML += row;
});
// Добавляем обработчики событий для кнопок
document.querySelectorAll('.edit-event-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const eventId = e.target.closest('tr').dataset.id;
window.location.href = `${PREFIX}/edit_event/${eventId}`;
});
});
document.querySelectorAll('.delete-event-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const eventId = e.target.closest('tr').dataset.id;
document.getElementById('delete-event-id').value = eventId;
new bootstrap.Modal('#deleteEventModal').show();
});
});
// Клик по строке события
document.querySelectorAll('.event-row').forEach(row => {
row.addEventListener('click', (e) => {
const eventId = row.dataset.id;
showEventDetails(eventId);
highlightSelectedEvent(eventId);
});
});
// Переинициализируем разделители колонок для событий
setTimeout(() => {
syncColumnWidths('events');
reinitializeColumnResizers('events');
}, 0);
// Автоматически выбираем событие из параметра URL
const urlParams = new URLSearchParams(window.location.search);
const selectedEventId = urlParams.get('selected_event');
if (selectedEventId) {
// Выделяем событие
highlightSelectedEvent(selectedEventId);
// Показываем детали события
showEventDetails(selectedEventId);
// Убираем параметр из URL без перезагрузки страницы
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
} catch (error) {
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i>
<p>Ошибка загрузки данных</p>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="loadEvents()">
Повторить попытку
</button>
</td>
</tr>`;
console.error('Ошибка загрузки событий:', error);
}
}
// Выделение выбранного события
function highlightSelectedEvent(eventId) {
// Снимаем выделение со всех событий
document.querySelectorAll('.event-row').forEach(row => {
row.classList.remove('active');
});
// Находим нужное событие
const selectedRow = document.querySelector(`.event-row[data-id="${eventId}"]`);
if (selectedRow) {
// Выделяем событие
selectedRow.classList.add('active');
selectedEventId = eventId;
// Сохраняем данные о выбранном событии
const eventName = selectedRow.querySelector('.event-name-preview').textContent;
selectedEventData = {
id: eventId,
name: eventName
};
// Прокручиваем к выбранному событию
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Обновляем информацию о выбранном событии на вкладке постов
updateSelectedEventInfo();
}
// Показать детали события
async function showEventDetails(eventId) {
const detailsPanel = document.getElementById('event-details');
const actionsPanel = document.getElementById('event-actions');
detailsPanel.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных события...</p>
</div>`;
try {
const response = await fetch(`${PREFIX}/api/events/${eventId}`);
if (!response.ok) {
throw new Error('Событие не найдено');
}
const event = await response.json();
// Форматируем даты
const formatDateTime = (dateStr) => {
if (!dateStr) return 'Не указано';
const date = new Date(dateStr);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// Форматируем булевы значения
const formatBoolean = (value) => {
return value ? 'Да' : 'Нет';
};
// Загружаем связанные посты
const linkedPosts = await fetch(`${PREFIX}/api/events/${eventId}/linked_posts`)
.then(response => response.json())
.catch(error => {
console.error('Ошибка загрузки связанных постов:', error);
return [];
});
// Создаем HTML для деталей
let detailsHtml = `
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Событие #${event.number} <small class="text-muted">(Local ID: ${event.id})</small></h4>
<div>
<button class="btn btn-sm btn-primary me-2" id="publish-event-btn">
<i class="fas fa-paper-plane me-1"></i>Опубликовать
</button>
<button class="btn btn-sm btn-info me-2" id="toggle-event-pub-btn">
${event.marked_to_publication ?
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации'}
</button>
<button class="btn btn-sm btn-warning" onclick="location.href='${PREFIX}/edit_event/${event.id}'">
<i class="fas fa-edit me-1"></i>Редактировать
</button>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Local ID:</div>
<div class="event-details-value">${event.id}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Площадка:</div>
<div class="event-details-value">${event.unit_name || 'Не указано'}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Название:</div>
<div class="event-details-value">${event.name || 'Не указано'}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-12">
<div class="event-details-label">Описание:</div>
<div class="event-details-value">${event.about || 'Не указано'}</div>
</div>
</div>
${event.about_social_picture ? `
<div class="row event-details-row">
<div class="col-12">
<div class="event-details-label">Изображение:</div>
<div class="text-center">
<img src="${PREFIX}/image_proxy?url=${encodeURIComponent(event.about_social_picture)}"
class="img-fluid detail-image rounded shadow-sm">
</div>
</div>
</div>` : ''}
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Тэги:</div>
<div class="event-details-value">${event.tags || 'Не указаны'}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Серверный ID:</div>
<div class="event-details-value">${event.id_event || 'Не указан'}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Номер события:</div>
<div class="event-details-value">${event.number || 'Не указан'}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Изменено:</div>
<div class="event-details-value">${formatDateTime(event.modified)}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Добавлено:</div>
<div class="event-details-value">${formatDateTime(event.added)}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Отменено:</div>
<div class="event-details-value">${formatBoolean(event.is_canceled)}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Принято:</div>
<div class="event-details-value">${formatBoolean(event.accepted)}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Отказано:</div>
<div class="event-details-value">${formatBoolean(event.denied)}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Видно:</div>
<div class="event-details-value">${formatBoolean(event.is_visible)}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Отмечено к публикации:</div>
<div class="event-details-value">${formatBoolean(event.marked_to_publication)}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Опубликовано в TG:</div>
<div class="event-details-value">${formatBoolean(event.is_posted_tg)}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">ID сообщения в TG:</div>
<div class="event-details-value">${event.tg_message_id || 'Не указан'}</div>
</div>
</div>
<div class="row event-details-row">
<div class="col-md-6">
<div class="event-details-label">Дата сообщения в TG:</div>
<div class="event-details-value">${formatDateTime(event.tg_posted_date)}</div>
</div>
<div class="col-md-6">
<div class="event-details-label">Ссылка на анонс:</div>
<div class="event-details-value">${event.announcement_link || 'Не указана'}</div>
</div>
</div>`;
// Добавляем блок связанных постов
if (linkedPosts.length > 0) {
detailsHtml += `
<div class="row event-details-row">
<div class="col-12">
<div class="event-details-label">Связанные посты:</div>
<div class="event-details-value linked-items-list">
<div class="list-group">
${linkedPosts.map(post => `
<a href="#" class="list-group-item list-group-item-action"
onclick="highlightSelectedPost(${post.id}); document.getElementById('posts-tab').click(); return false;">
${post.shortname || `Пост ${post.id}`}
<button class="btn btn-sm btn-outline-danger float-end"
onclick="event.stopPropagation(); unlinkEventFromPost(${event.id}, ${post.id}); return false;">
<i class="fas fa-unlink"></i>
</button>
</a>
`).join('')}
</div>
</div>
</div>
</div>`;
}
detailsHtml += `</div>`;
detailsPanel.innerHTML = detailsHtml;
// Обработчик для кнопки публикации события
document.getElementById('publish-event-btn').addEventListener('click', () => {
publishCurrentEvent(event.id);
});
// Обработчик для кнопки переключения публикации события
document.getElementById('toggle-event-pub-btn').addEventListener('click', () => {
toggleEventPublication(event.id);
});
} catch (error) {
detailsPanel.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Ошибка загрузки данных события: ${error.message}
<button class="btn btn-sm btn-outline-primary mt-2" onclick="showEventDetails(${eventId})">
Повторить попытку
</button>
</div>`;
console.error('Ошибка загрузки деталей события:', error);
}
}
// Функция загрузки постов
async function loadPosts() {
const tableBody = document.getElementById('posts-table');
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
Загрузка постов...
</td>
</tr>`;
try {
const response = await fetch(`${PREFIX}/api/posts`);
const posts = await response.json();
if (posts.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p>Нет доступных постов</p>
</td>
</tr>`;
return;
}
tableBody.innerHTML = '';
// Проверяем, нужно ли показывать колонку "Текст"
let showTextColumn = false;
posts.forEach(post => {
if (!post.shortname) {
showTextColumn = true;
}
});
// Показываем/скрываем колонку "Текст"
const textColumnHeader = document.getElementById('post-text-column-header');
textColumnHeader.style.display = showTextColumn ? 'table-cell' : 'none';
// Синхронизация после изменения видимости колонки
setTimeout(() => {
syncColumnWidths('posts');
reinitializeColumnResizers('posts');
}, 0);
posts.forEach(post => {
const statusBadge = post.marked_for_publication
? '<span class="badge bg-success status-badge">Для публикации</span>'
: '<span class="badge bg-secondary status-badge">Не для публикации</span>';
const publishedBadge = post.published_in_tg
? '<span class="badge bg-primary status-badge ms-1">Опубликовано</span>'
: '';
const eventBadge = post.is_event
? '<span class="badge bg-info status-badge ms-1">Событие</span>'
: '';
const pollBadge = post.is_poll
? '<span class="badge bg-warning status-badge ms-1">Опрос</span>'
: '';
// Показываем текст только если нет shortname
const textCell = showTextColumn
? `<td class="post-text-preview">${!post.shortname ? post.text_preview : ''}</td>`
: '';
const row = `
<tr class="post-row" data-id="${post.id}">
<td>${post.id}</td>
<td>${post.vk_post_id}</td>
<td class="shortname-preview" title="${post.shortname || ''}">${post.shortname || '-'}</td>
${textCell}
<td>${statusBadge}${publishedBadge}${eventBadge}${pollBadge}</td>
<td class="text-end action-buttons">
<button class="btn btn-sm btn-outline-primary view-btn">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-warning edit-btn">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`;
tableBody.innerHTML += row;
});
// Добавляем обработчики событий
document.querySelectorAll('.view-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const postId = e.target.closest('tr').dataset.id;
showPostDetails(postId);
highlightSelectedPost(postId);
});
});
document.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const postId = e.target.closest('tr').dataset.id;
window.location.href = `${PREFIX}/edit_post/${postId}`;
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const postId = e.target.closest('tr').dataset.id;
document.getElementById('delete-post-id').value = postId;
new bootstrap.Modal('#deleteModal').show();
});
});
// Клик по строке
document.querySelectorAll('.post-row').forEach(row => {
row.addEventListener('click', (e) => {
if (!e.target.closest('.action-buttons')) {
const postId = row.dataset.id;
showPostDetails(postId);
highlightSelectedPost(postId);
}
});
});
// Автоматически выбираем пост из параметра URL
const urlParams = new URLSearchParams(window.location.search);
const selectedPostId = urlParams.get('selected_post');
if (selectedPostId) {
// Выделяем пост
highlightSelectedPost(selectedPostId);
// Показываем детали поста
showPostDetails(selectedPostId);
// Убираем параметр из URL без перезагрузки страницы
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
} catch (error) {
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i>
<p>Ошибка загрузки данных</p>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="loadPosts()">
Повторить попытку
</button>
</td>
</tr>`;
console.error('Ошибка загрузки постов:', error);
}
}
// Выделение выбранного поста
function highlightSelectedPost(postId) {
// Снимаем выделение со всех постов
document.querySelectorAll('.post-row').forEach(row => {
row.classList.remove('active');
});
// Находим нужный пост
const selectedRow = document.querySelector(`.post-row[data-id="${postId}"]`);
if (selectedRow) {
// Выделяем пост
selectedRow.classList.add('active');
selectedPostId = postId;
// Сохраняем данные о выбранном посте
const postName = selectedRow.querySelector('.shortname-preview').textContent;
selectedPostData = {
id: postId,
name: postName
};
// Прокручиваем к выбранному посту
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Обновляем информацию о выбранном посте на вкладке событий
updateSelectedPostInfo();
}
// Показать детали поста
async function showPostDetails(postId) {
const detailsPanel = document.getElementById('post-details');
const actionsPanel = document.getElementById('post-actions');
detailsPanel.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных поста...</p>
</div>`;
try {
const response = await fetch(`${PREFIX}/api/posts/${postId}`);
if (!response.ok) {
throw new Error('Пост не найден');
}
const post = await response.json();
// Форматируем даты
const formatDate = (dateStr) => {
if (!dateStr) return 'Не указано';
const date = new Date(dateStr);
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
// Декодируем Unicode строки
const decodeUnicode = (str) => {
if (!str) return str;
return str.replace(/\\u[\dA-F]{4}/gi, (match) => {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
});
};
// Загружаем связанные события
const linkedEvents = await fetch(`${PREFIX}/api/posts/${postId}/linked_events`)
.then(response => response.json())
.catch(error => {
console.error('Ошибка загрузки связанных событий:', error);
return [];
});
// Создаем HTML для деталей
let detailsHtml = `
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div></div>
<div>
<button class="btn btn-sm btn-primary me-2" onclick="publishPost(${post.id})">
<i class="fas fa-paper-plane me-1"></i>Опубликовать
</button>
<button class="btn btn-sm btn-secondary me-2" onclick="generateShortname(${post.id})">
<i class="fas fa-font me-1"></i>Назвать
</button>
<button class="btn btn-sm btn-info me-2" id="toggle-pub-btn">
${post.marked_for_publication ?
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации'}
</button>
<button class="btn btn-sm btn-warning" onclick="location.href='${PREFIX}/edit_post/${post.id}'">
<i class="fas fa-edit me-1"></i>Редактировать
</button>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<div class="d-flex flex-wrap gap-2 mb-3">
${post.marked_for_publication && !post.published_in_tg ? '<span class="badge bg-warning">Для публикации</span>' : ''}
${post.published_in_tg ? '<span class="badge bg-primary">Опубликовано</span>' : ''}
${post.is_event ? '<span class="badge bg-info">Событие</span>' : ''}
${post.is_poll ? '<span class="badge bg-success">Опрос</span>' : ''}
${!post.marked_for_publication && !post.published_in_tg && !post.is_event && !post.is_poll ? '<span class="badge bg-secondary">Никакое</span>' : ''}
</div>
<div class="mb-2">
<strong>ВКонтакте:</strong> <a href="${post.vk_post_url || '#'}" target="_blank" class="text-decoration-none">${post.vk_post_url || 'Не указана'}</a>
</div>
<div class="mb-2">
<i class="fas fa-clock me-1"></i>${formatDate(post.published_at)}
</div>
${post.published_in_tg ? `
<div class="mb-2">
<strong>Телеграм</strong> <i class="fab fa-telegram-plane me-1"></i>${post.tg_message_id || 'Не указан'} <i class="fas fa-clock me-1"></i>${formatDate(post.tg_publication_date)}
</div>` : ''}
</div>
</div>
<div class="col-md-4">
${post.image_url ? `
<div class="text-center">
<img src="${PREFIX}/image_proxy?url=${encodeURIComponent(post.image_url)}"
class="img-fluid rounded shadow-sm" style="max-width: 33%; min-width: 200px;">
</div>` : ''}
</div>
</div>
<div class="mb-4 mt-3">
<div class="border p-3 bg-light rounded">${post.text || 'Нет текста'}</div>
</div>
${post.is_poll ? `
<div class="card border-primary mb-4">
<div class="card-header bg-primary text-white">
<i class="fas fa-poll me-1"></i> Информация об опросе
</div>
<div class="card-body p-2">
<div class="mb-1">
<small class="text-muted fw-bold">Вопрос опроса:</small>
<div class="bg-light p-2 rounded">${post.poll_question || 'Не указан'}</div>
</div>
<div class="mb-1">
<small class="text-muted fw-bold">Варианты ответов:</small>
<div class="bg-light p-2 rounded">${decodeUnicode(post.poll_options) || 'Не указаны'}</div>
</div>
<div class="row">
<div class="col-md-6">
<small class="text-muted fw-bold">Множественный выбор:</small>
<div class="bg-light p-2 rounded">${post.poll_multiple ? 'Да' : 'Нет'}</div>
</div>
<div class="col-md-6">
<small class="text-muted fw-bold">Окончание опроса:</small>
<div class="bg-light p-2 rounded">${formatDate(post.poll_end_date)}</div>
</div>
</div>
</div>
</div>` : ''}
${post.tg_poll_results ? `
<div class="card border-info mb-4">
<div class="card-header bg-info text-white">
<i class="fas fa-chart-bar me-1"></i> Результаты опроса в Telegram
</div>
<div class="card-body p-2">
<pre class="bg-light p-2 rounded mb-0" style="font-size: 0.85em; line-height: 1.3; max-height: 150px; overflow-y: auto;">${post.tg_poll_results}</pre>
</div>
</div>` : ''}
${post.marks && post.marks.length > 0 ? `
<div class="card border-success mb-4">
<div class="card-header bg-success text-white">
<i class="fas fa-users me-1"></i> Подписки (${post.marks.length})
</div>
<div class="card-body p-2">
<div class="table-responsive">
<table class="table table-sm mb-0" style="font-size: 0.9em;">
<thead>
<tr>
<th>Имя пользователя</th>
<th>ID пользователя</th>
<th>Дата подписки</th>
</tr>
</thead>
<tbody>
${post.marks.map(mark => `
<tr>
<td>${mark.tg_user_name || 'Не указано'}</td>
<td>${mark.tg_user_id || 'Не указан'}</td>
<td>${formatDate(mark.created_at)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>` : ''}`;
// Добавляем блок связанных событий
if (linkedEvents.length > 0) {
detailsHtml += `
<div class="card border-warning mb-4">
<div class="card-header bg-warning text-white">
<i class="fas fa-link me-1"></i> Связанные события (${linkedEvents.length})
</div>
<div class="card-body p-2">
<div class="linked-items-list">
<div class="list-group mb-0">
${linkedEvents.map(event => `
<a href="#" class="list-group-item list-group-item-action py-1"
onclick="highlightSelectedEvent(${event.id}); document.getElementById('events-tab').click(); return false;">
<small>${event.name || `Событие ${event.id}`}</small>
<button class="btn btn-sm btn-outline-danger float-end"
onclick="event.stopPropagation(); unlinkPostFromEvent(${post.id}, ${event.id}); return false;">
<i class="fas fa-unlink"></i>
</button>
</a>
`).join('')}
</div>
</div>
</div>
</div>`;
}
detailsHtml += `</div>`;
detailsPanel.innerHTML = detailsHtml;
// Обновляем заголовок панели
document.getElementById('post-details-header').textContent = `Пост #${post.vk_post_id}: ${post.shortname || 'Без названия'}`;
// Обработчик для кнопки публикации
document.getElementById('toggle-pub-btn').addEventListener('click', () => {
togglePublication(post.id);
});
} catch (error) {
detailsPanel.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Ошибка загрузки данных поста: ${error.message}
<button class="btn btn-sm btn-outline-primary mt-2" onclick="showPostDetails(${postId})">
Повторить попытку
</button>
</div>`;
console.error('Ошибка загрузки деталей поста:', error);
}
}
// Переключение статуса публикации
async function togglePublication(postId) {
const btn = document.getElementById('toggle-pub-btn');
const originalHtml = btn.innerHTML;
btn.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status"></span>
Обновление...
`;
btn.disabled = true;
try {
const response = await fetch(`${PREFIX}/api/posts/${postId}/toggle_publication`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
// Обновляем кнопку
btn.innerHTML = result.new_value ?
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации';
// Обновляем список постов
loadPosts();
} else {
throw new Error('Ошибка обновления статуса');
}
} catch (error) {
alert(`Ошибка: ${error.message}`);
} finally {
btn.disabled = false;
}
}
// Удаление поста
async function deletePost(postId) {
const deleteModal = bootstrap.Modal.getInstance('#deleteModal');
try {
const response = await fetch(`${PREFIX}/api/posts/${postId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
deleteModal.hide();
loadPosts();
// Сбрасываем детали поста
document.getElementById('post-details').innerHTML = `
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<p>Выберите пост для просмотра деталей</p>
</div>`;
} else {
throw new Error('Ошибка при удалении поста');
}
} catch (error) {
alert(`Ошибка удаления: ${error.message}`);
}
}
// Обновление информации о выбранном посте на вкладке событий
function updateSelectedPostInfo() {
const postInfoElement = document.getElementById('event-selected-post-info');
const postNameElement = document.getElementById('event-selected-post-name');
if (selectedPostData) {
postNameElement.textContent = selectedPostData.name || `Пост ${selectedPostData.id}`;
postInfoElement.style.display = 'flex';
updateInterlinkInfo();
} else {
postInfoElement.style.display = 'none';
}
}
// Обновление информации о выбранном событии на вкладке постов
function updateSelectedEventInfo() {
const eventInfoElement = document.getElementById('post-selected-event-info');
const eventNameElement = document.getElementById('post-selected-event-name');
if (selectedEventData) {
eventNameElement.textContent = selectedEventData.name || `Событие ${selectedEventData.id}`;
eventInfoElement.style.display = 'flex';
updateInterlinkInfo();
} else {
eventInfoElement.style.display = 'none';
}
}
// Добавляем функции для отвязывания постов от событий и наоборот
async function unlinkPostFromEvent(postId, eventId) {
if (!confirm('Вы уверены, что хотите отвязать это событие от поста?')) return;
try {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ post_id: postId, event_id: eventId })
});
const result = await response.json();
if (result.success) {
// Обновляем детали поста
showPostDetails(postId);
} else {
alert('Ошибка при отвязывании: ' + (result.error || 'неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка при отвязывании:', error);
alert('Произошла ошибка при отвязывании');
}
}
async function unlinkEventFromPost(eventId, postId) {
if (!confirm('Вы уверены, что хотите отвязать этот пост от события?')) return;
try {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ post_id: postId, event_id: eventId })
});
const result = await response.json();
if (result.success) {
// Обновляем детали события
showEventDetails(eventId);
} else {
alert('Ошибка при отвязывании: ' + (result.error || 'неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка при отвязывании:', error);
alert('Произошла ошибка при отвязывании');
}
}
// Сделаем функции глобальными для обработчиков в HTML
window.loadPosts = loadPosts;
window.loadEvents = loadEvents;
window.showPostDetails = showPostDetails;
window.showEventDetails = showEventDetails;
window.togglePublication = togglePublication;
window.toggleEventPublication = toggleEventPublication;
window.deletePost = deletePost;
window.deleteEvent = deleteEvent;
window.generateAllShortnames = generateAllShortnames;
window.generateShortname = generateShortname;
window.publishPost = publishPost;
window.publishCurrentEvent = publishCurrentEvent;
window.showProcessingMessage = showProcessingMessage;
window.hideProcessingMessage = hideProcessingMessage;
window.unlinkPostFromEvent = unlinkPostFromEvent;
window.unlinkEventFromPost = unlinkEventFromPost;
</script>
<script>
// Column resizer setup
function reinitializeColumnResizers(tableType) {
const selector = `#${tableType}-tab-pane .table-header-container thead th`;
const headers = document.querySelectorAll(selector);
headers.forEach((header) => {
const existingResizer = header.querySelector('.column-resizer');
if (existingResizer) {
existingResizer.remove();
}
const resizer = document.createElement('div');
resizer.classList.add('column-resizer');
header.style.position = 'relative';
header.appendChild(resizer);
setupColumnResizer(resizer, header, tableType);
});
}
// Column width adjustment
function setupColumnResizer(resizer, header, tableType = 'posts') {
let startX, startWidth, columnIndex, nextColumnIndex, startNextWidth;
// Определяем индекс колонки
const headerCells = Array.from(header.parentNode.children);
columnIndex = headerCells.indexOf(header);
nextColumnIndex = columnIndex + 1;
resizer.addEventListener('mousedown', (e) => {
startX = e.clientX;
const rect = header.getBoundingClientRect();
startWidth = rect.width;
// Получаем ширину следующей колонки
const nextHeader = headerCells[nextColumnIndex];
if (nextHeader) {
const nextRect = nextHeader.getBoundingClientRect();
startNextWidth = nextRect.width;
}
document.addEventListener('mousemove', resizeHandler);
document.addEventListener('mouseup', stopResize);
e.preventDefault();
e.stopPropagation();
});
function resizeHandler(e) {
const deltaX = e.clientX - startX;
const newWidth = Math.max(30, startWidth + deltaX); // Минимальная ширина 30px
// Вычисляем новую ширину следующей колонки
const nextWidth = Math.max(30, startNextWidth - deltaX);
// Применяем ширину к текущей колонке
header.style.width = `${newWidth}px`;
header.style.minWidth = `${newWidth}px`;
// Применяем ширину к следующей колонке
const nextHeader = headerCells[nextColumnIndex];
if (nextHeader) {
nextHeader.style.width = `${nextWidth}px`;
nextHeader.style.minWidth = `${nextWidth}px`;
}
// Применяем ширину к соответствующим ячейкам в теле таблицы
const bodyTable = document.querySelector(
tableType === 'posts'
? '#posts-tab-pane .scrollable-table-body table'
: '#events-tab-pane .scrollable-table-body table'
);
if (bodyTable) {
const bodyRows = bodyTable.querySelectorAll('tbody tr');
bodyRows.forEach(row => {
// Обновляем текущую колонку
if (row.cells && row.cells[columnIndex]) {
const bodyCell = row.cells[columnIndex];
if (window.getComputedStyle(bodyCell).display !== 'none') {
bodyCell.style.width = `${newWidth}px`;
bodyCell.style.minWidth = `${newWidth}px`;
}
}
// Обновляем следующую колонку
if (row.cells && row.cells[nextColumnIndex]) {
const nextBodyCell = row.cells[nextColumnIndex];
if (window.getComputedStyle(nextBodyCell).display !== 'none') {
nextBodyCell.style.width = `${nextWidth}px`;
nextBodyCell.style.minWidth = `${nextWidth}px`;
}
}
});
}
}
function stopResize() {
document.removeEventListener('mousemove', resizeHandler);
document.removeEventListener('mouseup', stopResize);
}
}
// Table column synchronization
function syncColumnWidths(tableType) {
// Определяем селекторы в зависимости от типа таблицы
let headerContainerSelector, bodyContainerSelector;
if (tableType === 'posts') {
headerContainerSelector = '#posts-tab-pane .table-header-container table';
bodyContainerSelector = '#posts-tab-pane .scrollable-table-body table';
} else if (tableType === 'events') {
headerContainerSelector = '#events-tab-pane .table-header-container table';
bodyContainerSelector = '#events-tab-pane .scrollable-table-body table';
} else {
console.warn('Неизвестный тип таблицы для синхронизации:', tableType);
return;
}
const headerTable = document.querySelector(headerContainerSelector);
const bodyTable = document.querySelector(bodyContainerSelector);
if (!headerTable || !bodyTable) {
return;
}
// Получаем видимые заголовки (исключая скрытые, например, колонку "Текст")
const allHeaderCells = headerTable.querySelectorAll('thead th');
const headerCells = Array.from(allHeaderCells).filter(cell => {
return window.getComputedStyle(cell).display !== 'none';
});
// Получаем строки тела
const bodyRows = bodyTable.querySelectorAll('tbody tr');
if (headerCells.length === 0) {
return;
}
// Получаем общую ширину контейнера таблицы
const containerWidth = headerTable.parentElement.offsetWidth;
// Вычисляем общую ширину всех колонок
let totalWidth = 0;
const columnWidths = [];
for (let i = 0; i < headerCells.length; i++) {
const headerCell = headerCells[i];
let computedWidth = 0;
const rect = headerCell.getBoundingClientRect();
computedWidth = rect.width;
if (computedWidth <= 0 || isNaN(computedWidth)) {
computedWidth = headerCell.offsetWidth;
}
computedWidth = Math.max(30, computedWidth); // Минимальная ширина
columnWidths.push(computedWidth);
totalWidth += computedWidth;
}
// Если общая ширина превышает контейнер, масштабируем все колонки
if (totalWidth > containerWidth) {
const scaleFactor = containerWidth / totalWidth;
columnWidths.forEach((width, index) => {
columnWidths[index] = Math.max(30, width * scaleFactor);
});
}
// Применяем вычисленные ширины
for (let i = 0; i < headerCells.length; i++) {
const headerCell = headerCells[i];
const width = columnWidths[i];
// Применяем ширину к заголовку
headerCell.style.width = `${width}px`;
headerCell.style.minWidth = `${width}px`;
// Применяем ширину к соответствующим ячейкам в теле таблицы
bodyRows.forEach(row => {
if (row.cells && row.cells[i]) {
const bodyCell = row.cells[i];
if (window.getComputedStyle(bodyCell).display !== 'none') {
bodyCell.style.width = `${width}px`;
bodyCell.style.minWidth = `${width}px`;
}
}
});
}
}
// Panel resizing setup
function setupPanelResizing() {
// Обрабатываем обе вкладки
['posts-tab-pane', 'events-tab-pane'].forEach(tabId => {
const tabPane = document.getElementById(tabId);
if (!tabPane) return;
const row = tabPane.querySelector('.row');
if (!row) return;
// Добавляем класс для flex-контейнера
row.classList.add('resizable-row');
// Находим левую и правую панели
const leftPanel = row.querySelector('.col-md-5');
const rightPanel = row.querySelector('.col-md-7');
if (!leftPanel || !rightPanel) return;
// Заменяем классы Bootstrap на наши собственные
leftPanel.classList.remove('col-md-5');
rightPanel.classList.remove('col-md-7');
leftPanel.classList.add('resizable-panel');
rightPanel.classList.add('resizable-panel');
// Создаем разделитель
const resizer = document.createElement('div');
resizer.classList.add('panel-resizer');
// Вставляем разделитель между панелями
leftPanel.after(resizer);
// Настройка обработчиков для разделителя
setupPanelResizer(resizer, leftPanel, rightPanel);
});
}
// Panel resizer handler
function setupPanelResizer(resizer, leftPanel, rightPanel) {
let startX, startLeftWidth, startRightWidth;
resizer.addEventListener('mousedown', (e) => {
startX = e.clientX;
startLeftWidth = parseInt(getComputedStyle(leftPanel).width);
startRightWidth = parseInt(getComputedStyle(rightPanel).width);
document.addEventListener('mousemove', resizeHandler);
document.addEventListener('mouseup', stopResize);
e.preventDefault();
});
function resizeHandler(e) {
const dx = e.clientX - startX;
const newLeftWidth = startLeftWidth + dx;
const newRightWidth = startRightWidth - dx;
// Минимальная ширина панелей
if (newLeftWidth > 100 && newRightWidth > 100) {
leftPanel.style.width = `${newLeftWidth}px`;
rightPanel.style.width = `${newRightWidth}px`;
// Column sync during resize (disabled for performance)
}
}
function stopResize() {
document.removeEventListener('mousemove', resizeHandler);
document.removeEventListener('mouseup', stopResize);
// Сохраняем настройки в localStorage
savePanelSizes();
// Final column sync after resize
const isPostsTab = leftPanel.closest('#posts-tab-pane') !== null;
const isEventsTab = leftPanel.closest('#events-tab-pane') !== null;
setTimeout(() => {
if (isPostsTab) {
syncColumnWidths('posts');
} else if (isEventsTab) {
syncColumnWidths('events');
}
}, 0);
}
}
// Panel size management
function savePanelSizes() {
const leftPanel = document.querySelector('#posts-tab-pane .resizable-panel:first-child');
const rightPanel = document.querySelector('#posts-tab-pane .resizable-panel:last-child');
if (leftPanel && rightPanel) {
localStorage.setItem('panelSizes', JSON.stringify({
left: leftPanel.style.width,
right: rightPanel.style.width
}));
}
}
// Функция для применения стандартного макета
function applyDefaultLayout() {
const container = document.querySelector('.container-fluid');
if (!container) return;
// Сбрасываем размеры панелей к значениям по умолчанию
const availableWidth = container.offsetWidth - 30; // Учитываем возможный padding
const defaultLeft = availableWidth * 0.4;
const defaultRight = availableWidth * 0.6;
document.querySelectorAll('.resizable-panel').forEach((panel, index) => {
// Применяем стандартные размеры
panel.style.width = (index % 2 === 0) ? `${defaultLeft}px` : `${defaultRight}px`;
// Сбрасываем возможные inline-стили min-width/max-width, если они мешают
panel.style.minWidth = '';
panel.style.maxWidth = '';
});
// Сбрасываем размеры колонок таблиц
// Для таблицы постов
const postsHeaderCells = document.querySelectorAll('#posts-tab-pane .table-header-container thead th');
const postsBodyRows = document.querySelectorAll('#posts-tab-pane .scrollable-table-body tbody tr');
if (postsHeaderCells.length > 0) {
// Скрываем колонку "Текст" по умолчанию (она показывается в loadPosts при необходимости)
const textColumnHeader = document.getElementById('post-text-column-header');
if (textColumnHeader) {
textColumnHeader.style.display = 'none';
}
// Сбрасываем inline-стили ширины для видимых заголовков
postsHeaderCells.forEach(cell => {
if (window.getComputedStyle(cell).display !== 'none') {
cell.style.width = '';
cell.style.minWidth = '';
}
});
// Сбрасываем inline-стили ширины для ячеек тела
postsBodyRows.forEach(row => {
Array.from(row.cells).forEach(cell => {
// Сбрасываем только для видимых ячеек
if (window.getComputedStyle(cell).display !== 'none') {
cell.style.width = '';
cell.style.minWidth = '';
}
});
});
// Синхронизируем после сброса
setTimeout(() => {
syncColumnWidths('posts');
}, 0);
}
// Для таблицы событий
const eventsHeaderCells = document.querySelectorAll('#events-tab-pane .table-header-container thead th');
const eventsBodyRows = document.querySelectorAll('#events-tab-pane .scrollable-table-body tbody tr');
if (eventsHeaderCells.length > 0) {
eventsHeaderCells.forEach(cell => {
cell.style.width = '';
cell.style.minWidth = '';
});
eventsBodyRows.forEach(row => {
Array.from(row.cells).forEach(cell => {
cell.style.width = '';
cell.style.minWidth = '';
});
});
setTimeout(() => {
syncColumnWidths('events');
}, 0);
}
// Сбрасываем сортировку в заголовках таблиц (удаляем классы asc/desc)
document.querySelectorAll('.sortable-header').forEach(header => {
header.classList.remove('asc', 'desc');
});
// Скрываем информацию о перекрестных связях, если она была показана
const crossTabInfos = document.querySelectorAll('.cross-tab-info');
crossTabInfos.forEach(info => {
// Проверяем, есть ли у нее стиль display, установленный в JS (например, flex)
// Проще всего просто скрыть, если данные не выбраны
// Или можно проверить selectedPostData/selectedEventData
// Для простоты скроем, они появятся при выборе
if (info.style.display !== 'none') {
info.style.display = 'none';
}
});
console.log("Настройки интерфейса сброшены к значениям по умолчанию.");
}
// Load panel sizes from localStorage
function loadPanelSizes() {
const savedSizes = localStorage.getItem('panelSizes');
const container = document.querySelector('.container-fluid');
if (!container) return;
// Функция для применения размеров
const applySizes = (leftWidth, rightWidth) => {
document.querySelectorAll('#posts-tab-pane .resizable-panel, #events-tab-pane .resizable-panel').forEach((panel, index) => {
// Применяем размеры: 0,2 - левая, 1,3 - правая
if (index % 2 === 0) { // Левая панель
panel.style.width = `${leftWidth}px`;
} else { // Правая панель
panel.style.width = `${rightWidth}px`;
}
});
// Sync columns after applying sizes
setTimeout(() => {
syncColumnWidths('posts');
syncColumnWidths('events');
}, 0);
};
if (savedSizes) {
try {
const sizes = JSON.parse(savedSizes);
// Получаем доступную ширину контейнера
const availableWidth = container.offsetWidth - 30; // Учитываем padding
// Проверяем, помещаются ли панели
const leftWidth = parseInt(sizes.left);
const rightWidth = parseInt(sizes.right);
if (!isNaN(leftWidth) && !isNaN(rightWidth) && leftWidth > 0 && rightWidth > 0) {
if (leftWidth + rightWidth > availableWidth) {
// Масштабируем размеры панелей
const scaleFactor = availableWidth / (leftWidth + rightWidth);
const newLeftWidth = Math.max(100, leftWidth * scaleFactor);
const newRightWidth = Math.max(100, rightWidth * scaleFactor);
// Применяем новые размеры
applySizes(newLeftWidth, newRightWidth);
} else {
// Применяем сохраненные размеры
applySizes(leftWidth, rightWidth);
}
} else {
// Если данные повреждены, применяем по умолчанию
throw new Error("Invalid saved sizes");
}
} catch (e) {
console.error("Ошибка при парсинге или применении сохраненных размеров панелей:", e);
// Применяем размеры по умолчанию при ошибке
const availableWidth = container.offsetWidth - 30;
const defaultLeft = availableWidth * 0.4;
const defaultRight = availableWidth * 0.6;
applySizes(defaultLeft, defaultRight);
}
} else {
// Установка размеров по умолчанию
const availableWidth = container.offsetWidth - 30;
const defaultLeft = availableWidth * 0.4;
const defaultRight = availableWidth * 0.6;
applySizes(defaultLeft, defaultRight);
}
}
// Layout reset functionality
function resetLayout() {
if (confirm("Вы уверены, что хотите сбросить настройки интерфейса к значениям по умолчанию?")) {
try {
// Удаляем данные из localStorage
localStorage.removeItem('panelSizes');
// Можно также удалить любые другие ключи, связанные с настройками интерфейса
// localStorage.removeItem('columnWidths_posts'); // Если бы использовались
// localStorage.removeItem('columnWidths_events'); // Если бы использовались
// Применяем стандартный макет
applyDefaultLayout();
alert("Настройки интерфейса успешно сброшены.");
} catch (e) {
console.error("Ошибка при сбросе настроек:", e);
alert("Произошла ошибка при сбросе настроек.");
}
}
}
// Table sorting and column resizing setup
function setupTableSorting() {
// Вспомогательная функция для сортировки строк
function sortRows(rows, columnIndex, direction) {
return rows.sort((a, b) => {
let aValue = a.cells[columnIndex].textContent.trim();
let bValue = b.cells[columnIndex].textContent.trim();
// Пытаемся преобразовать в числа, если возможно
if (!isNaN(aValue) && !isNaN(bValue)) {
aValue = parseFloat(aValue);
bValue = parseFloat(bValue);
}
if (direction === 'asc') {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});
}
// Функция для настройки разделителей колонок
function setupColumnResizers(headers, tableType) {
headers.forEach((header, index) => {
// Удаляем существующий разделитель, если есть
const existingResizer = header.querySelector('.column-resizer');
if (existingResizer) {
existingResizer.remove();
}
// Создаем новый разделитель
const resizer = document.createElement('div');
resizer.classList.add('column-resizer');
header.style.position = 'relative';
header.appendChild(resizer);
// Настраиваем обработчик изменения ширины колонки
setupColumnResizer(resizer, header, tableType);
});
}
// Настройка сортировки для таблицы постов
const postsHeaderTable = document.querySelector('#posts-tab-pane .table-header-container table');
const postsBodyTable = document.querySelector('#posts-tab-pane .scrollable-table-body table');
if (postsHeaderTable && postsBodyTable) {
const postsHeaders = postsHeaderTable.querySelectorAll('thead th');
postsHeaders.forEach((header, index) => {
if (header.classList.contains('text-end')) return; // Пропускаем колонку с действиями
header.classList.add('sortable-header');
header.addEventListener('click', () => {
// Определяем текущее направление сортировки
let direction = 'asc';
if (header.classList.contains('asc')) {
direction = 'desc';
} else if (header.classList.contains('desc')) {
direction = 'asc';
}
// Сбрасываем сортировку у всех заголовков
postsHeaders.forEach(h => {
h.classList.remove('asc', 'desc');
});
// Устанавливаем классы для визуального отображения
header.classList.add(direction);
// Получаем строки из тела таблицы
const tbody = postsBodyTable.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Сортируем строки
const sortedRows = sortRows(rows, index, direction);
// Очищаем и вставляем отсортированные строки
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
sortedRows.forEach(row => tbody.appendChild(row));
// Синхронизируем ширины после сортировки
syncColumnWidths('posts');
});
});
// Настраиваем разделители для всех колонок
setupColumnResizers(postsHeaders, 'posts');
}
// Настройка сортировки для таблицы событий
const eventsHeaderTable = document.querySelector('#events-tab-pane .table-header-container table');
const eventsBodyTable = document.querySelector('#events-tab-pane .scrollable-table-body table');
if (eventsHeaderTable && eventsBodyTable) {
const eventsHeaders = eventsHeaderTable.querySelectorAll('thead th');
eventsHeaders.forEach((header, index) => {
if (header.classList.contains('text-end')) return; // Пропускаем колонку с действиями
header.classList.add('sortable-header');
header.addEventListener('click', () => {
// Определяем текущее направление сортировки
let direction = 'asc';
if (header.classList.contains('asc')) {
direction = 'desc';
} else if (header.classList.contains('desc')) {
direction = 'asc';
}
// Сбрасываем сортировку у всех заголовков
eventsHeaders.forEach(h => {
h.classList.remove('asc', 'desc');
});
// Устанавливаем классы для визуального отображения
header.classList.add(direction);
// Получаем строки из тела таблицы
const tbody = eventsBodyTable.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
// Сортируем строки
const sortedRows = sortRows(rows, index, direction);
// Очищаем и вставляем отсортированные строки
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
sortedRows.forEach(row => tbody.appendChild(row));
// Синхронизируем ширины после сортировки
syncColumnWidths('events');
});
});
// Настраиваем разделители для всех колонок
setupColumnResizers(eventsHeaders, 'events');
}
}
// DOM initialization
document.addEventListener('DOMContentLoaded', () => {
// Инициализируем функционал после полной загрузки страницы
window.addEventListener('load', () => {
setupTableSorting();
setupPanelResizing();
loadPanelSizes();
// Добавляем обработчик события для кнопки сброса
const resetBtn = document.getElementById('reset-layout-btn');
if (resetBtn) {
resetBtn.addEventListener('click', (e) => {
e.preventDefault(); // Предотвращаем переход по ссылке
resetLayout();
});
}
// Синхронизируем ширины колонок после инициализации
setTimeout(() => {
syncColumnWidths('posts');
syncColumnWidths('events');
}, 100);
});
// Функция для пересчета ширины панелей при изменении размера окна
function adjustPanelWidthsOnResize() {
// Получаем доступную ширину из активной вкладки (она видима)
const activeTabPane = document.querySelector('.tab-pane.show.active');
if (!activeTabPane) return;
const activeContainer = activeTabPane.querySelector('.row');
if (!activeContainer) return;
const availableWidth = activeContainer.offsetWidth;
// Обновляем размеры всех вкладок с одинаковой доступной шириной
const allTabPanes = document.querySelectorAll('.tab-pane');
allTabPanes.forEach(tabPane => {
const leftPanel = tabPane.querySelector('.resizable-panel:first-child');
const rightPanel = tabPane.querySelector('.resizable-panel:last-child');
if (!leftPanel || !rightPanel) return;
// Применяем пропорциональные размеры (40% левая, 60% правая)
const leftWidth = Math.max(100, availableWidth * 0.4);
const rightWidth = Math.max(100, availableWidth * 0.6);
// Применяем новые ширины
leftPanel.style.width = `${leftWidth}px`;
rightPanel.style.width = `${rightWidth}px`;
});
// Синхронизируем колонки для активной вкладки
setTimeout(() => {
if (activeTabPane) {
const tabId = activeTabPane.id;
if (tabId === 'posts-tab-pane') {
syncColumnWidths('posts');
} else if (tabId === 'events-tab-pane') {
syncColumnWidths('events');
}
}
}, 0);
}
// Debounce для оптимизации производительности
let resizeTimeout;
// Обработчик изменения размера окна браузера
window.addEventListener('resize', () => {
// Очищаем предыдущий таймер
if (resizeTimeout) {
clearTimeout(resizeTimeout);
}
// Устанавливаем новый таймер
resizeTimeout = setTimeout(() => {
// Пересчитываем ширину панелей при изменении размера окна
adjustPanelWidthsOnResize();
}, 100); // Задержка 100ms
});
});
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
window.resetLayout = resetLayout;
window.applyDefaultLayout = applyDefaultLayout;
</script>
</body>
</html>