Files
Zilant2025/templates/index.html
T
2026-08-02 16:52:48 +03:00

4562 lines
227 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), .category-row:hover:not(.active) {
background-color: #f1f1f1;
}
.post-row.active, .event-row.active, .category-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, .category-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, .category-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, .category-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,
.category-row.active .category-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>
<!-- Плашка с режимом работы -->
<span class="badge bg-info ms-3" style="font-size: 0.9rem; padding: 0.5rem 0.75rem;">
{{ g.workmode }}
</span>
</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>
{% if g.workmode == 'VOLK' %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="categories-tab" data-bs-toggle="tab" data-bs-target="#categories-tab-pane" type="button" role="tab" aria-controls="categories-tab-pane" aria-selected="false">
<i class="fas fa-map-marker-alt me-1"></i> Площадки
</button>
</li>
{% endif %}
</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-warning me-2" id="mark-all-posts-pub-btn">
<i class="fas fa-check-double 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>
<button class="btn btn-secondary ms-2" id="test-post-btn">
<i class="fas fa-vial 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">
{% if g.workmode == 'VOLK' %}
<button class="btn btn-primary me-2" id="volk-rescan-btn">
<i class="fas fa-search me-1"></i> Рескан ВОЛК
</button>
{% endif %}
<button class="btn btn-success" id="add-event-btn">
<i class="fas fa-plus me-1"></i> Добавить событие
</button>
{% if g.workmode == 'ZILANT' %}
<button class="btn btn-primary" id="zk-rescan-btn">
<i class="fas fa-search me-1"></i> Рескан Зилант-сервера
</button>
{% endif %}
<button class="btn btn-warning me-2" id="mark-all-events-pub-btn">
<i class="fas fa-check-double 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>
{% if g.workmode == 'VOLK' %}
<!-- Вкладка площадок -->
<div class="tab-pane fade" id="categories-tab-pane" role="tabpanel" aria-labelledby="categories-tab" tabindex="0">
<div class="top-action-buttons d-flex align-items-center">
<button class="btn btn-warning me-2" id="mark-all-categories-pub-btn">
<i class="fas fa-check-double me-1"></i> Все - для публикации
</button>
<button class="btn btn-success me-2" id="publish-all-categories-btn">
<i class="fas fa-paper-plane me-1"></i> Опубликовать все
</button>
<button class="btn btn-info" id="load-categories-btn">
<i class="fas fa-sync-alt 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="categories-search" placeholder="Поиск...">
<button class="btn btn-outline-secondary btn-sm" type="button" id="categories-search-prev">
<i class="fas fa-chevron-up"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="categories-search-next">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<button class="btn btn-sm btn-primary" id="refresh-categories-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="text-end">Действия</th>
</tr>
</thead>
</table>
</div>
<!-- Контейнер для скроллируемого тела таблицы -->
<div class="scrollable-table-body">
<table class="table table-hover mb-0">
<tbody id="categories-table">
<tr>
<td colspan="4" 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="category-details-header">Детали площадки</h5>
<div id="category-actions"></div>
</div>
<div class="card-body scrollable-details" id="category-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>
{% endif %}
</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="deleteCategoryModal" 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 id="delete-category-message">Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.</p>
<input type="hidden" id="delete-category-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-category-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 }}";
const BOT_URL = "{{ g.webhook_url }}";
console.log("Lister Prefix is:", PREFIX); // Должно выводить "/zilant"
console.log("Bot Port is:", BOT_PORT); // Должно выводить порт бота
console.log("Bot URL is:", BOT_URL); // Должно выводить URL бота
// Текущий выбранный пост и событие
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('mark-all-posts-pub-btn').addEventListener('click', markAllPostsForPublication);
document.getElementById('publish-btn').addEventListener('click', runPublishScript);
document.getElementById('generate-names-btn').addEventListener('click', generateAllShortnames);
document.getElementById('test-post-btn').addEventListener('click', runTestPost);
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);
});
{% if g.workmode == 'VOLK' %}
const confirmCategoryDeleteBtn = document.getElementById('confirm-category-delete');
if (confirmCategoryDeleteBtn) {
confirmCategoryDeleteBtn.addEventListener('click', () => {
const categoryId = document.getElementById('delete-category-id').value;
deleteCategory(categoryId);
});
}
{% endif %}
// Обработчики для вкладок
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);
});
{% if g.workmode == 'VOLK' %}
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab) {
categoriesTab.addEventListener('click', () => {
if (document.getElementById('categories-table').children.length === 1) {
loadCategoriesList();
}
// Принудительно обновляем размеры панелей и синхронизируем колонки
setTimeout(() => {
adjustPanelWidthsOnResize();
syncColumnWidths('categories');
}, 50);
});
}
{% endif %}
// Новые обработчики для кнопок событий
{% if g.workmode == 'VOLK' %}
const volkRescanBtn = document.getElementById('volk-rescan-btn');
if (volkRescanBtn) {
volkRescanBtn.addEventListener('click', runVolkRescan);
}
{% endif %}
{% if g.workmode == 'ZILANT' %}
const zkRescanBtn = document.getElementById('zk-rescan-btn');
if (zkRescanBtn) {
zkRescanBtn.addEventListener('click', runZkRescan);
}
{% endif %}
document.getElementById('mark-all-events-pub-btn').addEventListener('click', markAllEventsForPublication);
document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents);
{% if g.workmode == 'VOLK' %}
const markAllCategoriesPubBtn = document.getElementById('mark-all-categories-pub-btn');
if (markAllCategoriesPubBtn) {
markAllCategoriesPubBtn.addEventListener('click', markAllCategoriesForPublication);
}
const publishAllCategoriesBtn = document.getElementById('publish-all-categories-btn');
if (publishAllCategoriesBtn) {
publishAllCategoriesBtn.addEventListener('click', publishAllCategories);
}
const loadCategoriesBtn = document.getElementById('load-categories-btn');
if (loadCategoriesBtn) {
loadCategoriesBtn.addEventListener('click', loadCategories);
}
const refreshCategoriesBtn = document.getElementById('refresh-categories-btn');
if (refreshCategoriesBtn) {
refreshCategoriesBtn.addEventListener('click', loadCategoriesList);
}
{% endif %}
// Обработчики для кнопок привязки
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 selectedCategoryId = urlParams.get('selected_category');
const tabParam = urlParams.get('tab');
// Если есть selectedCategoryId или параметр tab=categories, активируем вкладку площадок
{% if g.workmode == 'VOLK' %}
if (selectedCategoryId || tabParam === 'categories') {
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab) {
// Активируем вкладку площадок
const tabInstance = new bootstrap.Tab(categoriesTab);
tabInstance.show();
// Также загружаем площадки, если они еще не загружены
if (document.getElementById('categories-table').children.length === 1) {
loadCategoriesList();
}
}
}
{% endif %}
// Если есть 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 и selectedCategoryId
if (!selectedEventId && !selectedCategoryId && tabParam !== 'events' && tabParam !== 'categories') {
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);
}
{% if g.workmode == 'VOLK' %}
if (selectedCategoryId) {
// Ждем загрузки списка категорий перед выделением
setTimeout(() => {
highlightSelectedCategory(selectedCategoryId);
showCategoryDetails(selectedCategoryId);
// Убираем параметр из URL без перезагрузки страницы
const newUrl = window.location.pathname + (selectedPostId || selectedEventId ? window.location.search.replace(/[?&]selected_category=[^&]*/, '').replace(/[?&]tab=categories/, '') : '');
window.history.replaceState({}, document.title, newUrl);
}, 100);
}
{% endif %}
});
// Кнопка обновления постов
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 runVolkRescan() {
showProcessingMessage("Рескан ВОЛК...");
fetch(`${PREFIX}/api/run_volk_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 markAllPostsForPublication() {
if (!confirm('Вы уверены, что хотите отметить все посты для публикации?')) {
return;
}
showProcessingMessage("Отметка всех постов для публикации...");
fetch(`${PREFIX}/api/posts/mark_all_for_publication`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert(`Отмечено для публикации: ${data.count || 0} постов`);
loadPosts();
} else {
alert('Ошибка: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка:', error);
alert('Произошла ошибка при отметке постов');
});
}
// Отметить все события для публикации
function markAllEventsForPublication() {
if (!confirm('Вы уверены, что хотите отметить все события для публикации?')) {
return;
}
showProcessingMessage("Отметка всех событий для публикации...");
fetch(`${PREFIX}/api/events/mark_all_for_publication`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert(`Отмечено для публикации: ${data.count || 0} событий`);
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 loadCategories() {
// Сначала проверяем наличие событий
fetch(`${PREFIX}/api/events/count`)
.then(response => response.json())
.then(data => {
const eventCount = data.count || 0;
if (eventCount > 0) {
// Если есть события, спрашиваем подтверждение
if (!confirm(`В таблице событий есть ${eventCount} записей. Обновление данных площадок может повлиять на существующие события. Вы уверены, что хотите продолжить?`)) {
return;
}
}
// Продолжаем загрузку категорий
showProcessingMessage("Загрузка категорий...");
fetch(`${PREFIX}/api/load_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Категории загружены успешно!');
// Обновляем список категорий, если мы на вкладке площадок
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab && categoriesTab.classList.contains('active')) {
loadCategoriesList();
}
} else {
alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка загрузки категорий:', error);
alert('Произошла ошибка при загрузке категорий');
});
})
.catch(error => {
console.error('Ошибка проверки количества событий:', error);
// В случае ошибки все равно продолжаем загрузку
showProcessingMessage("Загрузка категорий...");
fetch(`${PREFIX}/api/load_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Категории загружены успешно!');
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab && categoriesTab.classList.contains('active')) {
loadCategoriesList();
}
} else {
alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(err => {
hideProcessingMessage();
console.error('Ошибка загрузки категорий:', err);
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 runTestPost() {
showProcessingMessage("Публикация тестового сообщения...");
fetch(`${PREFIX}/api/test_post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Тестовое сообщение успешно опубликовано!');
} 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(async response => {
let data = null;
try {
data = await response.json();
} catch (e) {
// Прокси мог оборвать долгий ответ (502/504 HTML) — название часто уже в БД
throw new Error(
`Сервер вернул не-JSON (HTTP ${response.status}). ` +
`Если запрос был долгим, название могло уже сохраниться — обновите список.`
);
}
if (!response.ok) {
throw new Error((data && data.error) || `HTTP ${response.status}`);
}
return data;
})
.then(async data => {
hideProcessingMessage();
if (data && data.success) {
alert('Название успешно сгенерировано: ' + data.shortname);
await loadPosts();
highlightSelectedPost(postId);
await showPostDetails(postId);
} else {
alert('Ошибка генерации названия: ' + ((data && data.error) || 'неизвестная ошибка'));
await loadPosts();
}
})
.catch(async error => {
hideProcessingMessage();
console.error('Ошибка генерации названия:', error);
// После таймаута прокси название часто уже записано — подтянем список
try {
await loadPosts();
highlightSelectedPost(postId);
await showPostDetails(postId);
} catch (e) {
console.error('Не удалось обновить список после ошибки:', e);
}
alert('Ошибка при генерации названия: ' + (error.message || error));
});
}
// Функция для публикации конкретного поста
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;">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>${post.shortname || `Пост ${post.id}`}</small>
<span class="badge ${post.status === 'Опубликован' ? 'bg-success' :
post.status === 'Для публикации' ? 'bg-warning' : 'bg-secondary'} ms-2">${post.status}</span>
</div>
<button class="btn btn-sm btn-outline-danger"
onclick="event.stopPropagation(); unlinkEventFromPost(${event.id}, ${post.id}); return false;">
<i class="fas fa-unlink"></i>
</button>
</div>
</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);
}
}
// Функции для работы с категориями (площадками)
let selectedCategoryId = null;
async function loadCategoriesList() {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
tableBody.innerHTML = `
<tr>
<td colspan="2" 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/categories`);
const categories = await response.json();
if (categories.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="text-center py-5 text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p>Нет доступных площадок</p>
</td>
</tr>`;
return;
}
tableBody.innerHTML = '';
categories.forEach(category => {
// Проверяем наличие полей и создаем бейджи
const markedForPub = category.marked_for_publication === true || category.marked_for_publication === 1;
const isPublished = category.is_tg_published === true || category.is_tg_published === 1;
const publicationBadge = markedForPub && !isPublished
? '<span class="badge bg-warning status-badge">Для публикации</span>'
: '';
const publishedBadge = isPublished
? '<span class="badge bg-primary status-badge ms-1">Опубликовано</span>'
: '';
const row = `
<tr class="category-row" data-id="${category.id}">
<td>${category.id || '-'}</td>
<td class="category-name-preview" title="${category.title || ''}">${category.title || 'Без названия'}</td>
<td>${publicationBadge}${publishedBadge}</td>
<td class="text-end action-buttons">
<button class="btn btn-sm btn-outline-warning edit-category-btn">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-category-btn">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`;
tableBody.innerHTML += row;
});
// Добавляем обработчики событий для кнопок
document.querySelectorAll('.edit-category-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const categoryId = e.target.closest('tr').dataset.id;
window.location.href = `${PREFIX}/edit_category/${encodeURIComponent(categoryId)}`;
});
});
document.querySelectorAll('.delete-category-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const categoryId = e.target.closest('tr').dataset.id;
document.getElementById('delete-category-id').value = categoryId;
// Проверяем наличие привязанных событий
checkCategoryEventsBeforeDelete(categoryId);
});
});
// Клик по строке категории
document.querySelectorAll('.category-row').forEach(row => {
row.addEventListener('click', (e) => {
if (!e.target.closest('.action-buttons')) {
const categoryId = row.dataset.id;
showCategoryDetails(categoryId);
highlightSelectedCategory(categoryId);
}
});
});
// Переинициализируем разделители колонок для категорий
setTimeout(() => {
syncColumnWidths('categories');
reinitializeColumnResizers('categories');
}, 0);
// Автоматически выбираем категорию из параметра URL
const urlParams = new URLSearchParams(window.location.search);
const selectedCategoryId = urlParams.get('selected_category');
if (selectedCategoryId) {
// Выделяем категорию
highlightSelectedCategory(selectedCategoryId);
// Показываем детали категории
showCategoryDetails(selectedCategoryId);
}
} catch (error) {
tableBody.innerHTML = `
<tr>
<td colspan="4" 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="loadCategoriesList()">
Повторить попытку
</button>
</td>
</tr>`;
console.error('Ошибка загрузки категорий:', error);
}
}
// Выделение выбранной категории
function highlightSelectedCategory(categoryId) {
// Снимаем выделение со всех категорий
document.querySelectorAll('.category-row').forEach(row => {
row.classList.remove('active');
});
// Находим нужную категорию
const selectedRow = document.querySelector(`.category-row[data-id="${categoryId}"]`);
if (selectedRow) {
// Выделяем категорию
selectedRow.classList.add('active');
selectedCategoryId = categoryId;
// Прокручиваем к выбранной категории
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Показать детали категории
async function showCategoryDetails(categoryId) {
const detailsPanel = document.getElementById('category-details');
if (!detailsPanel) return;
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/categories/${encodeURIComponent(categoryId)}`);
if (!response.ok) {
throw new Error('Площадка не найдена');
}
const category = await response.json();
// Форматируем дату из UTC в локальное время
const formatDateTime = (dateStr) => {
if (!dateStr) return '';
// Парсим дату как UTC и конвертируем в локальное время
const date = new Date(dateStr + (dateStr.includes('Z') ? '' : 'Z'));
return date.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
};
// Создаем 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-success me-2" id="publish-category-btn" ${!category.marked_for_publication || category.is_tg_published ? 'style="display: none;"' : ''}>
<i class="fas fa-paper-plane me-1"></i>Опубликовать
</button>
<button class="btn btn-sm btn-info me-2" id="toggle-category-pub-btn">
${category.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_category/${encodeURIComponent(category.id)}'">
<i class="fas fa-edit me-1"></i>Редактировать
</button>
</div>
</div>
<div class="row mb-3">
<div class="col-md-8">
<div class="mb-2">
<strong>ID:</strong> ${category.id || 'Не указано'}
</div>
<div class="mb-2">
<strong>Название:</strong> ${category.title || 'Не указано'}
</div>
<div class="mb-2">
<strong>Статус:</strong>
${category.marked_for_publication && !category.is_tg_published ? '<span class="badge bg-warning">Для публикации</span>' : ''}
${!category.marked_for_publication && !category.is_tg_published ? '<span class="badge bg-secondary">Не для публикации</span>' : ''}
${category.is_tg_published ? '<span class="badge bg-primary">Опубликовано</span>' : ''}
</div>
${category.is_tg_published ? `
<div class="mb-2">
<strong>Опубликовано в TG</strong> ${category.tg_id ? `[ID ${category.tg_id}]` : ''} ${category.tg_published_date ? formatDateTime(category.tg_published_date) : ''}
</div>
` : ''}
<div class="mb-2">
<strong>Количество событий:</strong> ${category.event_count || 0}
</div>
</div>
${category.image_url ? `
<div class="col-md-4">
<div id="category-image-container" style="text-align: right; display: none;">
<img id="category-image" src="${category.image_url}"
style="max-width: 200px; max-height: 200px; width: auto; height: auto; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);"
alt="Изображение площадки">
</div>
</div>
` : ''}
</div>`;
if (category.description) {
detailsHtml += `
<div class="mb-4 mt-3">
<strong>Описание:</strong>
<div class="border p-3 bg-light rounded mt-2">${category.description}</div>
</div>`;
}
detailsHtml += `</div>`;
detailsPanel.innerHTML = detailsHtml;
// Обработчик для кнопки публикации категории
const publishBtn = document.getElementById('publish-category-btn');
if (publishBtn) {
publishBtn.addEventListener('click', () => {
publishCategory(category.id);
});
}
// Обработчик для кнопки переключения публикации категории
const togglePubBtn = document.getElementById('toggle-category-pub-btn');
if (togglePubBtn) {
togglePubBtn.addEventListener('click', () => {
toggleCategoryPublication(category.id);
});
}
// Проверяем, загрузилось ли изображение
if (category.image_url) {
const img = document.getElementById('category-image');
const imgContainer = document.getElementById('category-image-container');
if (img && imgContainer) {
img.onload = function() {
// Изображение успешно загрузилось - показываем его
imgContainer.style.display = 'block';
};
img.onerror = function() {
// Изображение не загрузилось, скрываем контейнер
imgContainer.style.display = 'none';
};
}
}
// Обновляем заголовок панели
const header = document.getElementById('category-details-header');
if (header) {
header.textContent = `Площадка: ${category.title || category.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="showCategoryDetails('${categoryId}')">
Повторить попытку
</button>
</div>`;
console.error('Ошибка загрузки деталей категории:', error);
}
}
// Переключение статуса публикации категории
async function toggleCategoryPublication(categoryId) {
try {
const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}/toggle_publication`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
if (data.success) {
// Обновляем детали категории
showCategoryDetails(categoryId);
// Обновляем список категорий
loadCategoriesList();
} else {
alert('Ошибка изменения статуса публикации: ' + (data.error || 'неизвестная ошибка'));
}
} catch (error) {
console.error('Ошибка переключения статуса публикации:', error);
alert('Произошла ошибка при изменении статуса публикации');
}
}
// Проверка наличия событий перед удалением категории
async function checkCategoryEventsBeforeDelete(categoryId) {
try {
const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}/events_count`);
const data = await response.json();
const eventCount = data.count || 0;
if (eventCount > 0) {
const message = `У площадки есть ${eventCount} привязанных событий. Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.`;
document.getElementById('delete-category-message').textContent = message;
} else {
document.getElementById('delete-category-message').textContent = 'Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.';
}
const deleteModal = new bootstrap.Modal(document.getElementById('deleteCategoryModal'));
deleteModal.show();
} catch (error) {
console.error('Ошибка проверки событий:', error);
// Показываем модальное окно без проверки
document.getElementById('delete-category-message').textContent = 'Вы уверены, что хотите удалить эту площадку? Это действие нельзя отменить.';
const deleteModal = new bootstrap.Modal(document.getElementById('deleteCategoryModal'));
deleteModal.show();
}
}
// Удаление категории
async function deleteCategory(categoryId) {
try {
const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const result = await response.json();
if (result.success) {
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteCategoryModal'));
deleteModal.hide();
loadCategoriesList();
// Сбрасываем детали категории
document.getElementById('category-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 {
if (result.error === 'Cannot delete category with linked events') {
alert(`Невозможно удалить площадку: у неё есть ${result.event_count} привязанных событий.`);
} else {
alert('Ошибка удаления площадки: ' + (result.error || 'неизвестная ошибка'));
}
}
} catch (error) {
console.error('Ошибка удаления площадки:', error);
alert('Произошла ошибка при удалении площадки');
}
}
// Публикация категории
async function publishCategory(categoryId) {
showProcessingMessage("Публикация площадки...");
try {
const response = await fetch(`${PREFIX}/api/publish_category/${encodeURIComponent(categoryId)}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
const data = await response.json();
hideProcessingMessage();
if (data.success) {
alert('Площадка успешно опубликована!');
// Обновляем детали категории
showCategoryDetails(categoryId);
// Обновляем список категорий
loadCategoriesList();
} else {
alert('Ошибка публикации площадки: ' + (data.error || 'неизвестная ошибка'));
}
} catch (error) {
hideProcessingMessage();
console.error('Ошибка публикации площадки:', error);
alert('Произошла ошибка при публикации площадки');
}
}
// Отметить все категории для публикации
function markAllCategoriesForPublication() {
if (!confirm('Вы уверены, что хотите отметить все площадки для публикации?')) {
return;
}
showProcessingMessage("Отметка всех площадок для публикации...");
fetch(`${PREFIX}/api/categories/mark_all_for_publication`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert(`Отмечено для публикации: ${data.count || 0} площадок`);
loadCategoriesList();
} else {
alert('Ошибка: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка:', error);
alert('Произошла ошибка при отметке площадок');
});
}
// Публикация всех категорий
function publishAllCategories() {
const currentCategoryId = selectedCategoryId;
showProcessingMessage("Публикация всех площадок...");
fetch(`${PREFIX}/api/publish_all_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Публикация всех площадок выполнена успешно!');
loadCategoriesList().then(() => {
if (currentCategoryId) {
highlightSelectedCategory(currentCategoryId);
showCategoryDetails(currentCategoryId);
}
});
} else {
alert('Ошибка публикации всех площадок: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка публикации всех площадок:', error);
alert('Произошла ошибка при публикации всех площадок');
});
}
// Функция загрузки постов
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>
${post.auto_unit ? `
<div class="mb-2">
<strong>Площадка:</strong> ${post.auto_unit}
</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;">
<div class="d-flex justify-content-between align-items-center">
<div>
<small>${event.name || `Событие ${event.id}`}</small>
<span class="badge ${event.status === 'Опубликован' ? 'bg-success' :
event.status === 'Для публикации' ? 'bg-warning' :
event.status === 'Видим' ? 'bg-info' : 'bg-secondary'} ms-2">${event.status}</span>
</div>
<button class="btn btn-sm btn-outline-danger"
onclick="event.stopPropagation(); unlinkPostFromEvent(${post.id}, ${event.id}); return false;">
<i class="fas fa-unlink"></i>
</button>
</div>
</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.runTestPost = runTestPost;
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) {
showTelegramError('Пожалуйста, заполните оба поля: ID пользователя и сообщение', 'VALIDATION_ERROR');
return;
}
// Проверяем, что user_id является числом
if (isNaN(userId)) {
showTelegramError('ID пользователя должен быть числом', 'VALIDATION_ERROR');
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;
// Скрываем предыдущие ошибки
hideTelegramError();
const startTime = Date.now();
try {
// Используем URL из переменных окружения
const response = await fetch(`${BOT_URL}/send_message`, {
// const response = await fetch(`http://192.168.0.109:${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 responseTime = Date.now() - startTime;
if (response.ok) {
const result = await response.json();
if (result.success) {
showTelegramSuccess('Сообщение отправлено успешно!', responseTime);
// Очищаем поля
document.getElementById('telegram-user-id').value = '';
document.getElementById('telegram-message').value = '';
} else {
const errorCode = result.error_code || 'UNKNOWN_ERROR';
showTelegramError(`Ошибка отправки сообщения: ${result.error}`, 'API_ERROR', {
status: response.status,
responseTime: responseTime,
userId: userId,
errorCode: errorCode
});
}
} else {
// Обрабатываем HTTP ошибки
let errorText = '';
try {
const errorResult = await response.json();
errorText = errorResult.error || 'Неизвестная ошибка';
} catch (e) {
errorText = `HTTP ${response.status}: ${response.statusText}`;
}
showTelegramError(`Ошибка сервера: ${errorText}`, 'HTTP_ERROR', {
status: response.status,
statusText: response.statusText,
responseTime: responseTime,
userId: userId
});
}
} catch (error) {
const responseTime = Date.now() - startTime;
console.error('Ошибка при отправке сообщения:', error);
let errorType = 'NETWORK_ERROR';
let errorMessage = 'Ошибка сети при отправке сообщения';
if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage = 'Не удается подключиться к серверу бота. Проверьте, что бот запущен.';
errorType = 'CONNECTION_ERROR';
} else if (error.name === 'AbortError') {
errorMessage = 'Запрос был прерван по таймауту';
errorType = 'TIMEOUT_ERROR';
}
showTelegramError(errorMessage, errorType, {
errorName: error.name,
errorMessage: error.message,
responseTime: responseTime,
userId: userId
});
} finally {
// Восстанавливаем кнопку
sendBtn.innerHTML = originalText;
sendBtn.disabled = false;
}
}
// Функция для показа успешного результата
function showTelegramSuccess(message, responseTime) {
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-success alert-dismissible fade show position-fixed';
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
alertDiv.innerHTML = `
<div class="d-flex align-items-center">
<i class="fas fa-check-circle me-2"></i>
<div>
<strong>Успех!</strong><br>
${message}<br>
<small class="text-muted">Время ответа: ${responseTime}мс</small>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Автоматически скрываем через 5 секунд
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 5000);
}
// Функция для показа ошибки с подробной диагностикой
function showTelegramError(message, errorType, details = {}) {
const alertDiv = document.createElement('div');
alertDiv.className = 'alert alert-danger alert-dismissible fade show position-fixed';
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 400px; max-width: 600px;';
let diagnosticInfo = '';
let suggestions = '';
// Генерируем диагностическую информацию в зависимости от типа ошибки
switch (errorType) {
case 'VALIDATION_ERROR':
suggestions = '<strong>Решение:</strong> Проверьте правильность введенных данных';
break;
case 'API_ERROR':
diagnosticInfo = `
<div class="mt-2">
<small class="text-muted">
<strong>Диагностика:</strong><br>
• Статус: ${details.status || 'N/A'}<br>
• Время ответа: ${details.responseTime || 'N/A'}мс<br>
• ID пользователя: ${details.userId || 'N/A'}<br>
• Код ошибки: ${details.errorCode || 'N/A'}
</small>
</div>
`;
// Специфичные предложения в зависимости от кода ошибки
switch (details.errorCode) {
case 'INVALID_TOKEN':
suggestions = '<strong>Решение:</strong> Проблема с аутентификацией. Проверьте настройки токена в системе.';
break;
case 'NO_DATA':
case 'NO_USER_ID':
case 'NO_MESSAGE':
case 'INVALID_USER_ID':
suggestions = '<strong>Решение:</strong> Проверьте правильность заполнения полей формы.';
break;
case 'TELEGRAM_ERROR':
suggestions = '<strong>Возможные причины:</strong><br>• Пользователь заблокировал бота<br>• Неверный ID пользователя<br>• Пользователь не начал диалог с ботом<br>• Превышен лимит отправки сообщений';
break;
case 'INTERNAL_ERROR':
suggestions = '<strong>Решение:</strong> Внутренняя ошибка сервера. Проверьте логи бота или обратитесь к администратору.';
break;
default:
suggestions = '<strong>Возможные причины:</strong><br>• Пользователь заблокировал бота<br>• Неверный ID пользователя<br>• Пользователь не начал диалог с ботом';
}
break;
case 'HTTP_ERROR':
diagnosticInfo = `
<div class="mt-2">
<small class="text-muted">
<strong>Диагностика:</strong><br>
• HTTP статус: ${details.status || 'N/A'} ${details.statusText || ''}<br>
• Время ответа: ${details.responseTime || 'N/A'}мс<br>
• ID пользователя: ${details.userId || 'N/A'}
</small>
</div>
`;
if (details.status === 401) {
suggestions = '<strong>Решение:</strong> Проблема с аутентификацией. Проверьте настройки токена.';
} else if (details.status === 500) {
suggestions = '<strong>Решение:</strong> Внутренняя ошибка сервера. Проверьте логи бота.';
} else {
suggestions = '<strong>Решение:</strong> Проверьте статус сервера и настройки подключения.';
}
break;
case 'CONNECTION_ERROR':
suggestions = '<strong>Решение:</strong><br>• Проверьте, что бот запущен<br>• Проверьте сетевые настройки<br>• Убедитесь, что сервер доступен по адресу: ' + BOT_URL;
break;
case 'TIMEOUT_ERROR':
suggestions = '<strong>Решение:</strong> Попробуйте еще раз или проверьте стабильность соединения';
break;
case 'NETWORK_ERROR':
suggestions = '<strong>Решение:</strong> Проверьте интернет-соединение и доступность сервера';
break;
}
alertDiv.innerHTML = `
<div class="d-flex align-items-start">
<i class="fas fa-exclamation-triangle me-2 mt-1"></i>
<div class="flex-grow-1">
<strong>Ошибка отправки сообщения</strong><br>
${message}
${diagnosticInfo}
<div class="mt-2">
${suggestions}
</div>
</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alertDiv);
// Автоматически скрываем через 10 секунд для ошибок
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.remove();
}
}, 10000);
}
// Функция для скрытия ошибок
function hideTelegramError() {
const existingAlerts = document.querySelectorAll('.alert.position-fixed');
existingAlerts.forEach(alert => alert.remove());
}
// Добавляем обработчик события для кнопки отправки
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`;
}
// Применяем ширину к соответствующим ячейкам в теле таблицы
let bodyTableSelector;
if (tableType === 'posts') {
bodyTableSelector = '#posts-tab-pane .scrollable-table-body table';
} else if (tableType === 'events') {
bodyTableSelector = '#events-tab-pane .scrollable-table-body table';
} else if (tableType === 'categories') {
bodyTableSelector = '#categories-tab-pane .scrollable-table-body table';
} else {
bodyTableSelector = '#posts-tab-pane .scrollable-table-body table';
}
const bodyTable = document.querySelector(bodyTableSelector);
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 if (tableType === 'categories') {
headerContainerSelector = '#categories-tab-pane .table-header-container table';
bodyContainerSelector = '#categories-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');
} else if (tabId === 'categories-tab-pane') {
syncColumnWidths('categories');
}
}
}, 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);
}
}
// Простой поиск по событиям (без тэгов)
{% if g.workmode == 'VOLK' %}
// Функция поиска по категориям
function searchCategories(query) {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
const rows = tableBody.querySelectorAll('.category-row');
const searchTerm = query.toLowerCase().trim();
let foundCount = 0;
let firstMatch = null;
rows.forEach((row, index) => {
const id = row.querySelector('td:first-child').textContent.toLowerCase();
const name = row.querySelector('.category-name-preview').textContent.toLowerCase();
const matches = id.includes(searchTerm) || name.includes(searchTerm);
if (matches) {
row.style.display = '';
if (foundCount === 0) {
firstMatch = row;
}
foundCount++;
} else {
row.style.display = 'none';
}
});
// Выделяем первый найденный результат
if (firstMatch && searchTerm) {
const categoryId = firstMatch.dataset.id;
highlightSelectedCategory(categoryId);
showCategoryDetails(categoryId);
firstMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Навигация по результатам поиска категорий
let currentCategorySearchIndex = -1;
function navigateCategoriesSearch(direction) {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
const visibleRows = Array.from(tableBody.querySelectorAll('.category-row')).filter(row => row.style.display !== 'none');
if (visibleRows.length === 0) return;
if (direction === 'next') {
currentCategorySearchIndex = (currentCategorySearchIndex + 1) % visibleRows.length;
} else {
currentCategorySearchIndex = (currentCategorySearchIndex - 1 + visibleRows.length) % visibleRows.length;
}
const selectedRow = visibleRows[currentCategorySearchIndex];
const categoryId = selectedRow.dataset.id;
highlightSelectedCategory(categoryId);
showCategoryDetails(categoryId);
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
{% endif %}
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');
}
});
{% if g.workmode == 'VOLK' %}
const categoriesSearchInput = document.getElementById('categories-search');
if (categoriesSearchInput) {
categoriesSearchInput.addEventListener('input', (e) => {
searchCategories(e.target.value);
currentCategorySearchIndex = -1;
});
document.getElementById('categories-search-prev').addEventListener('click', () => {
navigateCategoriesSearch('prev');
});
document.getElementById('categories-search-next').addEventListener('click', () => {
navigateCategoriesSearch('next');
});
categoriesSearchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
navigateCategoriesSearch('next');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
navigateCategoriesSearch('prev');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigateCategoriesSearch('next');
}
});
}
{% endif %}
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>