Files
Zilant2025/templates/index.html
T

3401 lines
164 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 {
display: flex;
flex-direction: column;
}
/* Карточка детализации занимает всю высоту родительского контейнера */
.card.h-100 {
display: flex;
flex-direction: column;
}
.card.h-100 .card-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
min-height: 0;
}
/* Скроллируемая область детализации */
.scrollable-details {
position: absolute !important;
top: 60px !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
overflow-y: auto !important;
padding: 1rem !important;
margin: 0 !important;
box-sizing: border-box;
}
/* Контент внутри детализации */
.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;
}
/* Обеспечиваем одинаковую высоту для левой и правой панелей */
.row .col-md-5,
.row .col-md-7 {
display: flex;
flex-direction: column;
height: calc(100vh - 150px);
max-height: calc(100vh - 150px);
}
/* Ограничиваем высоту карточек */
.card.h-100 {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.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);
}
/* Стили для подсветки результатов поиска */
.search-highlight {
background-color: #fff3cd !important;
border-left: 4px solid #ffc107 !important;
animation: searchPulse 0.5s ease-in-out;
}
@keyframes searchPulse {
0% { background-color: #fff3cd; }
50% { background-color: #ffeaa7; }
100% { background-color: #fff3cd; }
}
/* Стили для полей поиска */
.input-group .form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.input-group .btn-outline-secondary:hover {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Управление постами VK и событиями</a>
<!-- Поля для отправки сообщений через бота -->
<div class="navbar-nav me-auto">
<div class="d-flex align-items-center">
<input type="text" class="form-control form-control-sm me-2" id="telegram-user-id" placeholder="ID пользователя" style="width: 150px;">
<input type="text" class="form-control form-control-sm me-2" id="telegram-message" placeholder="Сообщение" style="width: 200px;">
<button class="btn btn-outline-light btn-sm" id="send-telegram-btn">
<i class="fas fa-paper-plane me-1"></i> Отправить
</button>
</div>
</div>
<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>
<button class="btn btn-sm btn-outline-success ms-2" id="post-venues-btn" style="display: none;">
<i class="fas fa-map-marker-alt me-1"></i> Площадки
</button>
<button class="btn btn-sm btn-outline-info ms-2" id="post-tags-btn" style="display: none;">
<i class="fas fa-tags me-1"></i> Тэги
</button>
</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 class="d-flex align-items-center">
<div class="input-group me-2" style="width: 200px;">
<input type="text" class="form-control form-control-sm" id="posts-search" placeholder="Поиск...">
<button class="btn btn-outline-secondary btn-sm" type="button" id="posts-search-prev">
<i class="fas fa-chevron-up"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="posts-search-next">
<i class="fas fa-chevron-down"></i>
</button>
</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 class="d-flex align-items-center">
<div class="input-group me-2" style="width: 200px;">
<input type="text" class="form-control form-control-sm" id="events-search" placeholder="Поиск...">
<button class="btn btn-outline-secondary btn-sm" type="button" id="events-search-prev">
<i class="fas fa-chevron-up"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="events-search-next">
<i class="fas fa-chevron-down"></i>
</button>
</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" id="event-details-header">Детали события</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 class="modal fade" id="venuesModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<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">
<div class="list-group" id="venues-list" style="max-height: 60vh; overflow-y: auto;">
<!-- Список площадок будет загружен динамически -->
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора событий площадки -->
<div class="modal fade" id="eventsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">События площадки: <span id="selected-venue-name"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-primary me-2" id="select-all-events">
<i class="fas fa-check-square me-1"></i> Выбрать все
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselect-all-events">
<i class="fas fa-square me-1"></i> Снять все
</button>
</div>
<div class="list-group" id="events-list" style="max-height: 50vh; overflow-y: auto;">
<!-- Список событий будет загружен динамически -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirm-events-link">ОК</button>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора тэгов -->
<div class="modal fade" id="tagsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<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">
<div class="list-group" id="tags-list" style="max-height: 60vh; overflow-y: auto;">
<!-- Список тэгов будет загружен динамически -->
</div>
</div>
</div>
</div>
</div>
<!-- Модальное окно для выбора событий по тэгу -->
<div class="modal fade" id="tagEventsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">События с тэгом: <span id="selected-tag-name"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<button type="button" class="btn btn-sm btn-outline-primary me-2" id="select-all-tag-events">
<i class="fas fa-check-square me-1"></i> Выбрать все
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="deselect-all-tag-events">
<i class="fas fa-square me-1"></i> Снять все
</button>
</div>
<div class="list-group" id="tag-events-list" style="max-height: 50vh; overflow-y: auto;">
<!-- Список событий будет загружен динамически -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirm-tag-events-link">ОК</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 }}";
const BOT_PORT = "{{ g.bot_port }}";
console.log("Lister Prefix is:", PREFIX); // Должно выводить "/zilant"
console.log("Bot Port is:", BOT_PORT); // Должно выводить порт бота
// Текущий выбранный пост и событие
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();
}
// Обновляем видимость кнопки "Площадки"
updateVenuesButtonVisibility();
// Принудительно обновляем размеры панелей и синхронизируем колонки
setTimeout(() => {
adjustPanelWidthsOnResize();
syncColumnWidths('posts');
}, 50);
});
document.getElementById('events-tab').addEventListener('click', () => {
if (document.getElementById('events-table').children.length === 1) {
loadEvents();
updateSelectedPostInfo();
}
// Сбрасываем выбранный пост при переключении на вкладку событий
selectedPostId = null;
selectedPostData = null;
updateVenuesButtonVisibility();
// Принудительно обновляем размеры панелей и синхронизируем колонки
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">
<div></div>
<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">
<div class="col-md-8">
<div class="mb-3">
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
<div class="border rounded p-2 d-flex gap-2 align-items-center">
<span class="${event.accepted ? 'fw-bold' : 'text-decoration-line-through'}">Принято</span>
<span class="${event.denied ? 'fw-bold' : 'text-decoration-line-through'}">Отказано</span>
<span class="${event.is_canceled ? 'fw-bold' : 'text-decoration-line-through'}">Отменено</span>
${event.is_visible ? '<span class="badge bg-success">Видно</span>' : '<span class="badge bg-secondary">Не видно</span>'}
</div>
${event.marked_to_publication && !event.is_posted_tg ? '<span class="badge bg-warning align-middle">Для публикации</span>' : ''}
${!event.marked_to_publication && !event.is_posted_tg ? '<span class="badge bg-secondary align-middle">Не для публикации</span>' : ''}
${event.is_posted_tg ? '<span class="badge bg-primary align-middle">Опубликовано</span>' : ''}
</div>
<div class="mb-2">
<strong>Площадка:</strong> ${event.unit_name || 'Не указано'}
</div>
<div class="mb-2">
<strong>Тэги:</strong> ${event.tags || 'Не указаны'}
</div>
<div class="mb-2">
<strong>Создано</strong> <i class="fas fa-clock me-1"></i>${formatDateTime(event.added)}
</div>
<div class="mb-2">
<strong>Обновлено</strong> <i class="fas fa-clock me-1"></i>${formatDateTime(event.modified)}
</div>
${event.is_posted_tg ? `
<div class="mb-2">
<strong>Телеграм</strong> <i class="fab fa-telegram-plane me-1"></i>${event.tg_message_id || 'Не указан'} <i class="fas fa-clock me-1"></i>${formatDateTime(event.tg_posted_date)}
</div>` : ''}
</div>
</div>
<div class="col-md-4">
${event.about_social_picture ? `
<div class="text-center">
<img src="${PREFIX}/image_proxy?url=${encodeURIComponent(event.about_social_picture)}"
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">${event.about || 'Нет описания'}</div>
</div>`;
// Добавляем блок связанных постов
if (linkedPosts.length > 0) {
detailsHtml += `
<div class="card border-info mb-4">
<div class="card-header bg-info text-white">
<i class="fas fa-link me-1"></i> Связанные посты (${linkedPosts.length})
</div>
<div class="card-body p-2">
<div class="linked-items-list">
<div class="list-group mb-0">
${linkedPosts.map(post => `
<a href="#" class="list-group-item list-group-item-action py-1"
onclick="highlightSelectedPost(${post.id}); document.getElementById('posts-tab').click(); return false;">
<small>${post.shortname || `Пост ${post.id}`}</small>
<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('event-details-header').textContent = `Событие #${event.number}: ${event.name || 'Без названия'}`;
// Обработчик для кнопки публикации события
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();
// Обновляем информацию о выбранном событии на вкладке постов (для показа кнопки "Площадки")
updateSelectedEventInfo();
// Показываем кнопку "Площадки" при выборе поста
updateVenuesButtonVisibility();
}
// Показать детали поста
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';
}
}
// Обновление видимости кнопки "Площадки"
function updateVenuesButtonVisibility() {
const venuesBtn = document.getElementById('post-venues-btn');
const tagsBtn = document.getElementById('post-tags-btn');
if (selectedPostId) {
venuesBtn.style.display = 'inline-block';
tagsBtn.style.display = 'inline-block';
} else {
venuesBtn.style.display = 'none';
tagsBtn.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;
// Функция для отправки сообщения через Telegram бота
async function sendTelegramMessage() {
const userId = document.getElementById('telegram-user-id').value.trim();
const message = document.getElementById('telegram-message').value.trim();
// Проверяем, что оба поля заполнены
if (!userId || !message) {
alert('Пожалуйста, заполните оба поля: ID пользователя и сообщение');
return;
}
// Проверяем, что user_id является числом
if (isNaN(userId)) {
alert('ID пользователя должен быть числом');
return;
}
// Показываем индикатор загрузки
const sendBtn = document.getElementById('send-telegram-btn');
const originalText = sendBtn.innerHTML;
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Отправка...';
sendBtn.disabled = true;
try {
const response = await fetch(`http://localhost:${BOT_PORT}/send_message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Secret-Token': '{{ g.webhook_secret }}',
},
body: JSON.stringify({
user_id: parseInt(userId),
message: message
})
});
const result = await response.json();
if (result.success) {
alert('Сообщение отправлено успешно!');
// Очищаем поля
document.getElementById('telegram-user-id').value = '';
document.getElementById('telegram-message').value = '';
} else {
alert('Ошибка отправки сообщения: ' + result.error);
}
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
alert('Произошла ошибка при отправке сообщения. Проверьте консоль для подробностей.');
} finally {
// Восстанавливаем кнопку
sendBtn.innerHTML = originalText;
sendBtn.disabled = false;
}
}
// Добавляем обработчик события для кнопки отправки
document.getElementById('send-telegram-btn').addEventListener('click', sendTelegramMessage);
// Добавляем обработчик для отправки по Enter в полях
document.getElementById('telegram-user-id').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
document.getElementById('telegram-message').focus();
}
});
document.getElementById('telegram-message').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendTelegramMessage();
}
});
</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
});
// Функции для работы с площадками
let selectedVenueName = '';
let selectedVenueEvents = [];
let currentPostId = null;
// Обработчик клика по кнопке "Площадки"
document.getElementById('post-venues-btn').addEventListener('click', async () => {
if (!selectedPostId) {
alert('Сначала выберите пост');
return;
}
currentPostId = selectedPostId;
await loadVenues();
const venuesModal = new bootstrap.Modal(document.getElementById('venuesModal'));
venuesModal.show();
});
// Загрузка списка площадок
async function loadVenues() {
try {
const response = await fetch(`${PREFIX}/api/venues`);
const venues = await response.json();
const venuesList = document.getElementById('venues-list');
venuesList.innerHTML = '';
if (venues.length === 0) {
venuesList.innerHTML = '<div class="list-group-item text-center text-muted">Площадки не найдены</div>';
return;
}
venues.forEach(venue => {
const venueItem = document.createElement('div');
venueItem.className = 'list-group-item list-group-item-action';
venueItem.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${venue.name}</h6>
<small class="text-muted">${venue.event_count} событий</small>
</div>
`;
venueItem.addEventListener('click', () => {
selectedVenueName = venue.name;
loadVenueEvents(venue.name);
});
venuesList.appendChild(venueItem);
});
} catch (error) {
console.error('Ошибка загрузки площадок:', error);
alert('Ошибка загрузки площадок');
}
}
// Загрузка событий выбранной площадки
async function loadVenueEvents(venueName) {
try {
const response = await fetch(`${PREFIX}/api/venues/${encodeURIComponent(venueName)}/events?post_id=${currentPostId}`);
const events = await response.json();
selectedVenueEvents = events;
// Закрываем модальное окно площадок
const venuesModal = bootstrap.Modal.getInstance(document.getElementById('venuesModal'));
venuesModal.hide();
// Показываем модальное окно событий
document.getElementById('selected-venue-name').textContent = venueName;
renderEventsList(events);
const eventsModal = new bootstrap.Modal(document.getElementById('eventsModal'));
eventsModal.show();
} catch (error) {
console.error('Ошибка загрузки событий площадки:', error);
alert('Ошибка загрузки событий площадки');
}
}
// Отображение списка событий с чекбоксами
function renderEventsList(events) {
const eventsList = document.getElementById('events-list');
eventsList.innerHTML = '';
if (events.length === 0) {
eventsList.innerHTML = '<div class="list-group-item text-center text-muted">События не найдены</div>';
return;
}
events.forEach(event => {
const eventItem = document.createElement('div');
eventItem.className = 'list-group-item';
eventItem.innerHTML = `
<div class="form-check">
<input class="form-check-input" type="checkbox" value="${event.id}" id="event-${event.id}" ${event.linked ? 'checked' : ''}>
<label class="form-check-label" for="event-${event.id}">
<strong>${event.name}</strong>
${event.number ? `<br><small class="text-muted">№ ${event.number}</small>` : ''}
</label>
</div>
`;
eventsList.appendChild(eventItem);
});
}
// Обработчики кнопок "Выбрать все" и "Снять все"
document.getElementById('select-all-events').addEventListener('click', () => {
document.querySelectorAll('#events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
});
});
document.getElementById('deselect-all-events').addEventListener('click', () => {
document.querySelectorAll('#events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
});
// Обработчик кнопки "ОК" для подтверждения привязки/отвязки событий
document.getElementById('confirm-events-link').addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('#events-list input[type="checkbox"]');
const promises = [];
for (const checkbox of checkboxes) {
const eventId = parseInt(checkbox.value);
const isChecked = checkbox.checked;
const wasLinked = selectedVenueEvents.find(e => e.id === eventId)?.linked || false;
// Если состояние изменилось, выполняем соответствующее действие
if (isChecked && !wasLinked) {
// Привязываем событие к посту
promises.push(linkEventToPost(currentPostId, eventId));
} else if (!isChecked && wasLinked) {
// Отвязываем событие от поста
promises.push(unlinkEventFromPost(currentPostId, eventId));
}
}
try {
await Promise.all(promises);
alert('Изменения сохранены');
// Закрываем модальное окно
const eventsModal = bootstrap.Modal.getInstance(document.getElementById('eventsModal'));
eventsModal.hide();
// Обновляем информацию о связях
updateInterlinkInfo();
// Обновляем детали выбранного поста, если он открыт
if (currentPostId) {
await showPostDetails(currentPostId);
}
} catch (error) {
console.error('Ошибка при сохранении изменений:', error);
alert('Ошибка при сохранении изменений');
}
});
// Функция привязки события к посту
async function linkEventToPost(postId, eventId) {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
post_id: postId,
event_id: eventId
})
});
if (!response.ok) {
throw new Error('Ошибка привязки события к посту');
}
}
// Функция отвязки события от поста
async function unlinkEventFromPost(postId, eventId) {
const response = await fetch(`${PREFIX}/api/interlinks`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
post_id: postId,
event_id: eventId
})
});
if (!response.ok) {
throw new Error('Ошибка отвязки события от поста');
}
}
// Функции для работы с тэгами
let selectedTagName = '';
let selectedTagEvents = [];
// Обработчик клика по кнопке "Тэги"
document.getElementById('post-tags-btn').addEventListener('click', async () => {
if (!selectedPostId) {
alert('Сначала выберите пост');
return;
}
currentPostId = selectedPostId;
await loadTags();
const tagsModal = new bootstrap.Modal(document.getElementById('tagsModal'));
tagsModal.show();
});
// Загрузка списка тэгов
async function loadTags() {
try {
const response = await fetch(`${PREFIX}/api/tags`);
const tags = await response.json();
const tagsList = document.getElementById('tags-list');
tagsList.innerHTML = '';
if (tags.length === 0) {
tagsList.innerHTML = '<div class="list-group-item text-center text-muted">Тэги не найдены</div>';
return;
}
tags.forEach(tag => {
const tagItem = document.createElement('div');
tagItem.className = 'list-group-item list-group-item-action';
tagItem.innerHTML = `
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${tag.name}</h6>
<small class="text-muted">${tag.event_count} событий</small>
</div>
`;
tagItem.addEventListener('click', () => {
selectedTagName = tag.name;
loadTagEvents(tag.name);
});
tagsList.appendChild(tagItem);
});
} catch (error) {
console.error('Ошибка загрузки тэгов:', error);
alert('Ошибка загрузки тэгов');
}
}
// Загрузка событий выбранного тэга
async function loadTagEvents(tagName) {
try {
const response = await fetch(`${PREFIX}/api/tags/${encodeURIComponent(tagName)}/events?post_id=${currentPostId}`);
const events = await response.json();
selectedTagEvents = events;
// Закрываем модальное окно тэгов
const tagsModal = bootstrap.Modal.getInstance(document.getElementById('tagsModal'));
tagsModal.hide();
// Показываем модальное окно событий
document.getElementById('selected-tag-name').textContent = tagName;
renderTagEventsList(events);
const tagEventsModal = new bootstrap.Modal(document.getElementById('tagEventsModal'));
tagEventsModal.show();
} catch (error) {
console.error('Ошибка загрузки событий тэга:', error);
alert('Ошибка загрузки событий тэга');
}
}
// Отображение списка событий с чекбоксами для тэгов
function renderTagEventsList(events) {
const eventsList = document.getElementById('tag-events-list');
eventsList.innerHTML = '';
if (events.length === 0) {
eventsList.innerHTML = '<div class="list-group-item text-center text-muted">События не найдены</div>';
return;
}
events.forEach(event => {
const eventItem = document.createElement('div');
eventItem.className = 'list-group-item';
eventItem.innerHTML = `
<div class="form-check">
<input class="form-check-input" type="checkbox" value="${event.id}" id="tag-event-${event.id}" ${event.linked ? 'checked' : ''}>
<label class="form-check-label" for="tag-event-${event.id}">
<strong>${event.name}</strong>
${event.number ? `<br><small class="text-muted">№ ${event.number}</small>` : ''}
${event.unit_name ? `<br><small class="text-muted">Площадка: ${event.unit_name}</small>` : ''}
</label>
</div>
`;
eventsList.appendChild(eventItem);
});
}
// Обработчики кнопок "Выбрать все" и "Снять все" для тэгов
document.getElementById('select-all-tag-events').addEventListener('click', () => {
document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = true;
});
});
document.getElementById('deselect-all-tag-events').addEventListener('click', () => {
document.querySelectorAll('#tag-events-list input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
});
// Обработчик кнопки "ОК" для подтверждения привязки/отвязки событий по тэгу
document.getElementById('confirm-tag-events-link').addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('#tag-events-list input[type="checkbox"]');
const promises = [];
for (const checkbox of checkboxes) {
const eventId = parseInt(checkbox.value);
const isChecked = checkbox.checked;
const wasLinked = selectedTagEvents.find(e => e.id === eventId)?.linked || false;
// Если состояние изменилось, выполняем соответствующее действие
if (isChecked && !wasLinked) {
// Привязываем событие к посту
promises.push(linkEventToPost(currentPostId, eventId));
} else if (!isChecked && wasLinked) {
// Отвязываем событие от поста
promises.push(unlinkEventFromPost(currentPostId, eventId));
}
}
try {
await Promise.all(promises);
alert('Изменения сохранены');
// Закрываем модальное окно
const tagEventsModal = bootstrap.Modal.getInstance(document.getElementById('tagEventsModal'));
tagEventsModal.hide();
// Обновляем информацию о связях
updateInterlinkInfo();
// Обновляем детали выбранного поста, если он открыт
if (currentPostId) {
await showPostDetails(currentPostId);
}
} catch (error) {
console.error('Ошибка при сохранении изменений:', error);
alert('Ошибка при сохранении изменений');
}
});
// Функции поиска
let postsSearchResults = [];
let eventsSearchResults = [];
let currentPostsSearchIndex = -1;
let currentEventsSearchIndex = -1;
// Поиск по постам
function searchPosts(query) {
if (!query.trim()) {
postsSearchResults = [];
currentPostsSearchIndex = -1;
clearPostsSearchHighlight();
return;
}
const posts = document.querySelectorAll('.post-row');
postsSearchResults = [];
posts.forEach((post, index) => {
const postId = post.dataset.id;
const shortname = post.querySelector('.shortname-preview')?.textContent || '';
const textPreview = post.querySelector('.post-text-preview')?.textContent || '';
const searchText = `${shortname} ${textPreview}`.toLowerCase();
if (searchText.includes(query.toLowerCase())) {
postsSearchResults.push({
element: post,
index: index,
id: postId
});
}
});
currentPostsSearchIndex = postsSearchResults.length > 0 ? 0 : -1;
highlightSearchResults('posts');
}
// Поиск по событиям (расширенный с тэгами)
async function searchEvents(query) {
if (!query.trim()) {
eventsSearchResults = [];
currentEventsSearchIndex = -1;
clearEventsSearchHighlight();
return;
}
try {
// Получаем все события из API для поиска по тэгам
const response = await fetch(`${PREFIX}/api/events`);
const allEvents = await response.json();
const events = document.querySelectorAll('.event-row');
eventsSearchResults = [];
events.forEach((event, index) => {
const eventId = event.dataset.id;
const name = event.querySelector('.event-name-preview')?.textContent || '';
const unitName = event.querySelector('.shortname-preview')?.textContent || '';
// Находим полные данные события для поиска по тэгам
const eventData = allEvents.find(e => e.id == eventId);
const tags = eventData?.tags || '';
const searchText = `${name} ${unitName} ${tags}`.toLowerCase();
if (searchText.includes(query.toLowerCase())) {
eventsSearchResults.push({
element: event,
index: index,
id: eventId
});
}
});
currentEventsSearchIndex = eventsSearchResults.length > 0 ? 0 : -1;
highlightSearchResults('events');
} catch (error) {
console.error('Ошибка при поиске событий:', error);
// Fallback к простому поиску без тэгов
searchEventsSimple(query);
}
}
// Простой поиск по событиям (без тэгов)
function searchEventsSimple(query) {
const events = document.querySelectorAll('.event-row');
eventsSearchResults = [];
events.forEach((event, index) => {
const eventId = event.dataset.id;
const name = event.querySelector('.event-name-preview')?.textContent || '';
const unitName = event.querySelector('.shortname-preview')?.textContent || '';
const searchText = `${name} ${unitName}`.toLowerCase();
if (searchText.includes(query.toLowerCase())) {
eventsSearchResults.push({
element: event,
index: index,
id: eventId
});
}
});
currentEventsSearchIndex = eventsSearchResults.length > 0 ? 0 : -1;
highlightSearchResults('events');
}
// Подсветка результатов поиска
function highlightSearchResults(type) {
if (type === 'posts') {
clearPostsSearchHighlight();
if (currentPostsSearchIndex >= 0 && currentPostsSearchIndex < postsSearchResults.length) {
const result = postsSearchResults[currentPostsSearchIndex];
result.element.classList.add('search-highlight');
result.element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Выделяем найденный пост
highlightSelectedPost(result.id);
// Показываем детали поста в правой панели
showPostDetails(result.id);
}
} else if (type === 'events') {
clearEventsSearchHighlight();
if (currentEventsSearchIndex >= 0 && currentEventsSearchIndex < eventsSearchResults.length) {
const result = eventsSearchResults[currentEventsSearchIndex];
result.element.classList.add('search-highlight');
result.element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Выделяем найденное событие
highlightSelectedEvent(result.id);
// Показываем детали события в правой панели
showEventDetails(result.id);
}
}
}
// Очистка подсветки поиска постов
function clearPostsSearchHighlight() {
document.querySelectorAll('.post-row.search-highlight').forEach(row => {
row.classList.remove('search-highlight');
});
}
// Очистка подсветки поиска событий
function clearEventsSearchHighlight() {
document.querySelectorAll('.event-row.search-highlight').forEach(row => {
row.classList.remove('search-highlight');
});
}
// Навигация по результатам поиска постов
function navigatePostsSearch(direction) {
if (postsSearchResults.length === 0) return;
if (direction === 'next') {
currentPostsSearchIndex = (currentPostsSearchIndex + 1) % postsSearchResults.length;
} else if (direction === 'prev') {
currentPostsSearchIndex = currentPostsSearchIndex <= 0 ? postsSearchResults.length - 1 : currentPostsSearchIndex - 1;
}
highlightSearchResults('posts');
}
// Навигация по результатам поиска событий
function navigateEventsSearch(direction) {
if (eventsSearchResults.length === 0) return;
if (direction === 'next') {
currentEventsSearchIndex = (currentEventsSearchIndex + 1) % eventsSearchResults.length;
} else if (direction === 'prev') {
currentEventsSearchIndex = currentEventsSearchIndex <= 0 ? eventsSearchResults.length - 1 : currentEventsSearchIndex - 1;
}
highlightSearchResults('events');
}
// Обработчики событий для поиска постов
document.getElementById('posts-search').addEventListener('input', (e) => {
searchPosts(e.target.value);
});
document.getElementById('posts-search-prev').addEventListener('click', () => {
navigatePostsSearch('prev');
});
document.getElementById('posts-search-next').addEventListener('click', () => {
navigatePostsSearch('next');
});
// Обработчики событий для поиска событий
document.getElementById('events-search').addEventListener('input', (e) => {
searchEvents(e.target.value);
});
document.getElementById('events-search-prev').addEventListener('click', () => {
navigateEventsSearch('prev');
});
document.getElementById('events-search-next').addEventListener('click', () => {
navigateEventsSearch('next');
});
// Обработчики клавиш для поиска
document.getElementById('posts-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
navigatePostsSearch('next');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
navigatePostsSearch('prev');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigatePostsSearch('next');
}
});
document.getElementById('events-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
navigateEventsSearch('next');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
navigateEventsSearch('prev');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigateEventsSearch('next');
}
});
});
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
window.resetLayout = resetLayout;
window.applyDefaultLayout = applyDefaultLayout;
</script>
</body>
</html>