987 lines
42 KiB
HTML
987 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Органайзер АйТи-Депо</title>
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/jstree.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <!-- Библиотека для Markdown -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
|
||
<style>
|
||
body {
|
||
display: flex;
|
||
font-family: Arial, sans-serif;
|
||
margin: 0;
|
||
transition: background-color 0.3s, color 0.3s;
|
||
}
|
||
body.dark-theme {
|
||
background-color: #1a1a1a;
|
||
color: #e0e0e0;
|
||
}
|
||
#tree-container {
|
||
width: 15%;
|
||
padding: 10px;
|
||
border-right: 1px solid #ccc;
|
||
}
|
||
#tree-container.dark-theme { border-right-color: #444; }
|
||
#installs-container {
|
||
width: 60%;
|
||
padding: 10px;
|
||
position: relative;
|
||
}
|
||
.install-item {
|
||
display: table;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 2px 0;
|
||
background: #fff;
|
||
cursor: move;
|
||
color: #000; /* Цвет текста в светлой теме */
|
||
}
|
||
.install-item.dark-theme {
|
||
background: #2a2a2a;
|
||
color: #e0e0e0; /* Цвет текста в тёмной теме */
|
||
}
|
||
.install-item.selected { background-color: #e6f7ff; }
|
||
.install-item.selected.dark-theme { background-color: #003a6d; }
|
||
.install-item > div {
|
||
display: table-cell;
|
||
vertical-align: middle;
|
||
padding: 5px;
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
.install-item .computer-name {
|
||
width: 20%;
|
||
max-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.install-item .connection-id {
|
||
width: 30%;
|
||
max-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.install-item .install-time {
|
||
width: 25%;
|
||
max-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.install-item .actions { width: 25%; }
|
||
.form-container { margin-bottom: 20px; }
|
||
.jstree-node { position: relative; }
|
||
.folder-actions {
|
||
display: none;
|
||
position: absolute;
|
||
right: 5px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
margin-left: 0;
|
||
}
|
||
.jstree-default-dark .jstree-clicked {
|
||
background: #005580 !important;
|
||
color: #e0e0e0 !important;
|
||
}
|
||
.jstree-default-dark .jstree-hovered {
|
||
background: #003d66 !important;
|
||
color: #e0e0e0 !important;
|
||
}
|
||
#search-container {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
text-align: right;
|
||
}
|
||
#search-container.dark-theme input, #search-container.dark-theme select {
|
||
background-color: #333;
|
||
color: #e0e0e0;
|
||
border: 1px solid #555;
|
||
}
|
||
#search-input { width: 200px; margin-bottom: 5px; }
|
||
#installs-list { margin-top: 0; }
|
||
.header {
|
||
display: table;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: #f0f0f0;
|
||
cursor: pointer;
|
||
color: #000; /* Цвет текста в светлой теме */
|
||
}
|
||
.header.dark-theme {
|
||
background: #333;
|
||
color: #e0e0e0; /* Цвет текста в тёмной теме */
|
||
}
|
||
.header > div {
|
||
display: table-cell;
|
||
padding: 5px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
}
|
||
.header > div:hover {
|
||
background: #e0e0e0;
|
||
color: #000; /* Тёмный текст на светлом фоне в светлой теме */
|
||
}
|
||
.header.dark-theme > div:hover {
|
||
background: #444;
|
||
color: #e0e0e0; /* Светлый текст на тёмном фоне в темной теме */
|
||
}
|
||
.sort-arrow { font-size: 12px; margin-left: 5px; }
|
||
.protocol-icon {
|
||
margin-right: 5px;
|
||
height: 16px;
|
||
width: 16px;
|
||
vertical-align: middle;
|
||
}
|
||
.connection-link {
|
||
color: blue;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
margin-right: 5px;
|
||
}
|
||
.connection-link.dark-theme { color: #66b3ff; }
|
||
.edit-button {
|
||
background: #fff;
|
||
color: #007bff;
|
||
border: 1px solid #007bff;
|
||
padding: 2px 5px;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
margin: 0 2px;
|
||
transition: background 0.2s ease;
|
||
}
|
||
.edit-button.dark-theme {
|
||
background: #2a2a2a;
|
||
color: #66b3ff;
|
||
border-color: #66b3ff;
|
||
}
|
||
.edit-button:hover {
|
||
background: #e0e0e0;
|
||
color: #0056b3;
|
||
}
|
||
.edit-button.dark-theme:hover {
|
||
background: #444;
|
||
color: #99ccff;
|
||
}
|
||
.copy-button {
|
||
background: #fff;
|
||
color: #28a745;
|
||
border: 1px solid #28a745;
|
||
padding: 2px 5px;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
margin: 0 2px;
|
||
transition: background 0.2s ease;
|
||
}
|
||
.copy-button.dark-theme {
|
||
background: #2a2a2a;
|
||
color: #5cb85c;
|
||
border-color: #5cb85c;
|
||
}
|
||
.copy-button:hover {
|
||
background: #e0e0e0;
|
||
color: #218838;
|
||
}
|
||
.copy-button.dark-theme:hover {
|
||
background: #444;
|
||
color: #8de28d;
|
||
}
|
||
.action-buttons { margin-left: 5px; }
|
||
.action-buttons button { margin: 0 2px; }
|
||
#import-form { margin-top: 5px; }
|
||
#import-file { display: none; }
|
||
#import-label {
|
||
cursor: pointer;
|
||
background: #007bff;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 3px;
|
||
margin-left: 5px;
|
||
}
|
||
#import-label.dark-theme {
|
||
background: #004d99;
|
||
border-color: #004d99;
|
||
}
|
||
#import-label:hover { background: #0056b3; }
|
||
#import-label.dark-theme:hover { background: #0066cc; }
|
||
.folder-select { margin-bottom: 5px; width: 150px; }
|
||
.time-hint { font-size: 12px; color: #666; margin-top: 5px; }
|
||
.time-hint.dark-theme { color: #bbb; }
|
||
#notes-panel {
|
||
width: 25%;
|
||
padding: 10px;
|
||
border-left: 1px solid #ccc;
|
||
background: #f9f9f9;
|
||
min-height: 200px;
|
||
margin-top: 240px;
|
||
}
|
||
#notes-panel.dark-theme {
|
||
border-left-color: #444;
|
||
background: #222;
|
||
}
|
||
#notes-content { margin-top: 10px; }
|
||
#note-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: white;
|
||
padding: 20px;
|
||
border: 1px solid #ccc;
|
||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
width: 500px;
|
||
}
|
||
#note-modal.dark-theme {
|
||
background: #2a2a2a;
|
||
border-color: #444;
|
||
box-shadow: 0 0 10px rgba(0,0,0,0.5);
|
||
}
|
||
#note-modal .markdown-buttons { margin-bottom: 10px; }
|
||
#note-modal .markdown-buttons button { margin-right: 5px; padding: 5px 10px; }
|
||
#note-modal textarea { width: 100%; height: 200px; margin-bottom: 10px; }
|
||
#note-modal.dark-theme textarea {
|
||
background: #333;
|
||
color: #e0e0e0;
|
||
border: 1px solid #555;
|
||
}
|
||
#note-modal button { margin-right: 10px; }
|
||
#note-modal.dark-theme button {
|
||
background: #007bff;
|
||
color: white;
|
||
border-color: #007bff;
|
||
}
|
||
#note-modal button:hover { background: #0056b3; }
|
||
#note-modal.dark-theme button:hover { background: #004d99; }
|
||
#modal-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 999;
|
||
}
|
||
.header-logo { display: flex; align-items: center; }
|
||
.header-logo img { height: 50px; margin-right: 10px; }
|
||
.theme-toggle {
|
||
cursor: pointer;
|
||
background: #007bff;
|
||
color: white;
|
||
border: 1px solid #007bff;
|
||
padding: 5px 10px;
|
||
border-radius: 3px;
|
||
margin-left: 10px;
|
||
transition: background 0.2s ease;
|
||
}
|
||
.theme-toggle:hover {
|
||
background: #0056b3;
|
||
}
|
||
.theme-toggle.dark-theme {
|
||
background: #004d99;
|
||
border-color: #004d99;
|
||
}
|
||
.theme-toggle.dark-theme:hover {
|
||
background: #0066cc;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="tree-container">
|
||
<h2>Папки</h2>
|
||
<div id="folder-tree"></div>
|
||
<button onclick="addFolder()">Добавить папку</button>
|
||
</div>
|
||
<div id="installs-container">
|
||
<h1 class="header-logo">
|
||
<img src="/icons/it-depo-logo.png" alt="АйТи-Депо логотип"> <!-- Логотип -->
|
||
Органайзер АйТи-Депо
|
||
<button class="theme-toggle" onclick="toggleTheme()">Темная тема</button>
|
||
</h1>
|
||
<div id="search-container">
|
||
<input type="text" id="search-input" placeholder="Поиск по ID или имени">
|
||
<select id="folder-select" class="folder-select">
|
||
<option value="">Все папки</option>
|
||
</select>
|
||
<button onclick="exportCSV()">Экспорт CSV</button>
|
||
<form id="import-form" enctype="multipart/form-data">
|
||
<input type="file" id="import-file" accept=".csv" onchange="importCSV(this.files[0])">
|
||
<label for="import-file" id="import-label">Импорт CSV</label>
|
||
</form>
|
||
</div>
|
||
<div class="form-container">
|
||
<h2>Добавить запись</h2>
|
||
<input type="text" id="rustId" placeholder="ID подключения" required>
|
||
<input type="text" id="computerName" placeholder="Имя компьютера" required>
|
||
<input type="text" id="installTime" placeholder="Время (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС)">
|
||
<select id="protocol">
|
||
<option value="rustdesk" selected>RustDesk</option>
|
||
<option value="anydesk">AnyDesk</option>
|
||
<option value="ammyy">Ammyy Admin</option>
|
||
<option value="teamviewer">TeamViewer</option>
|
||
<option value="vnc">VNC</option>
|
||
<option value="rdp">RDP</option>
|
||
</select>
|
||
<textarea id="note" placeholder="Заметка (Markdown поддерживается)"></textarea>
|
||
<button onclick="addInstall()">Добавить</button>
|
||
<div class="time-hint">Пример: 2025-03-05 14:30:00</div>
|
||
</div>
|
||
<div class="header">
|
||
<div onclick="sortInstalls('computer_name')">Имя компьютера<span class="sort-arrow"></span></div>
|
||
<div onclick="sortInstalls('rust_id')">ID подключения<span class="sort-arrow"></span></div>
|
||
<div onclick="sortInstalls('install_time')">Время установки<span class="sort-arrow"></span></div>
|
||
<div>Действия</div>
|
||
</div>
|
||
<div id="installs-list"></div>
|
||
</div>
|
||
<div id="notes-panel">
|
||
<h2>Заметка</h2>
|
||
<div id="notes-content"></div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для редактирования заметок -->
|
||
<div id="modal-overlay"></div>
|
||
<div id="note-modal">
|
||
<h3>Редактировать заметку</h3>
|
||
<div class="markdown-buttons">
|
||
<button onclick="formatBold()">Жирный</button>
|
||
<button onclick="formatItalic()">Курсив</button>
|
||
<button onclick="formatLink()">Ссылка</button>
|
||
</div>
|
||
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
|
||
<button onclick="saveNote()">Сохранить</button>
|
||
<button onclick="closeNoteModal()">Закрыть</button>
|
||
</div>
|
||
|
||
<script>
|
||
const API_URL = "/api"; // Замени <server-ip> на IP или домен сервера
|
||
let selectedFolderId = null;
|
||
let allInstalls = [];
|
||
let allFolders = []; // Добавляем для отслеживания папок
|
||
let selectedInstallId = null;
|
||
let sortField = null;
|
||
let sortDirection = 'asc';
|
||
|
||
const protocolIcons = {
|
||
'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">',
|
||
'anydesk': '<img src="/icons/anydesk.png" class="protocol-icon">',
|
||
'ammyy': '<img src="/icons/ammyy.png" class="protocol-icon">',
|
||
'teamviewer': '<img src="/icons/teamviewer.png" class="protocol-icon">',
|
||
'vnc': '<img src="/icons/vnc.png" class="protocol-icon">',
|
||
'rdp': '<img src="/icons/rdp.png" class="protocol-icon">'
|
||
};
|
||
|
||
const protocolLinks = {
|
||
'rustdesk': 'rustdesk://',
|
||
'anydesk': 'anydesk://',
|
||
'ammyy': 'ammyy://',
|
||
'teamviewer': 'teamviewer://remote-control/',
|
||
'vnc': 'vnc://',
|
||
'rdp': 'rdp://'
|
||
};
|
||
|
||
// Проверка и применение сохранённой темы
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const savedTheme = localStorage.getItem('theme');
|
||
if (savedTheme === 'dark') {
|
||
document.body.classList.add('dark-theme');
|
||
document.getElementById('installs-container').classList.add('dark-theme');
|
||
document.getElementById('tree-container').classList.add('dark-theme');
|
||
document.getElementById('notes-panel').classList.add('dark-theme');
|
||
document.getElementById('note-modal').classList.add('dark-theme');
|
||
document.getElementById('search-container').classList.add('dark-theme');
|
||
document.getElementById('import-label').classList.add('dark-theme');
|
||
document.querySelector('.theme-toggle').classList.add('dark-theme');
|
||
document.querySelector('.theme-toggle').textContent = 'Светлая тема';
|
||
// Применяем темную тему к уже загруженным элементам таблицы
|
||
document.querySelectorAll('.install-item').forEach(item => item.classList.add('dark-theme'));
|
||
document.querySelector('.header').classList.add('dark-theme');
|
||
// Добавляем класс для темной темы в jstree
|
||
$('#folder-tree').addClass('jstree-default-dark');
|
||
}
|
||
});
|
||
|
||
function toggleTheme() {
|
||
const body = document.body;
|
||
const installsContainer = document.getElementById('installs-container');
|
||
const treeContainer = document.getElementById('tree-container');
|
||
const notesPanel = document.getElementById('notes-panel');
|
||
const noteModal = document.getElementById('note-modal');
|
||
const searchContainer = document.getElementById('search-container');
|
||
const importLabel = document.getElementById('import-label');
|
||
const themeToggle = document.querySelector('.theme-toggle');
|
||
const header = document.querySelector('.header');
|
||
const installItems = document.querySelectorAll('.install-item');
|
||
const folderTree = $('#folder-tree');
|
||
|
||
if (body.classList.contains('dark-theme')) {
|
||
body.classList.remove('dark-theme');
|
||
installsContainer.classList.remove('dark-theme');
|
||
treeContainer.classList.remove('dark-theme');
|
||
notesPanel.classList.remove('dark-theme');
|
||
noteModal.classList.remove('dark-theme');
|
||
searchContainer.classList.remove('dark-theme');
|
||
importLabel.classList.remove('dark-theme');
|
||
themeToggle.classList.remove('dark-theme');
|
||
header.classList.remove('dark-theme');
|
||
installItems.forEach(item => item.classList.remove('dark-theme'));
|
||
folderTree.removeClass('jstree-default-dark');
|
||
themeToggle.textContent = 'Темная тема';
|
||
localStorage.setItem('theme', 'light');
|
||
} else {
|
||
body.classList.add('dark-theme');
|
||
installsContainer.classList.add('dark-theme');
|
||
treeContainer.classList.add('dark-theme');
|
||
notesPanel.classList.add('dark-theme');
|
||
noteModal.classList.add('dark-theme');
|
||
searchContainer.classList.add('dark-theme');
|
||
importLabel.classList.add('dark-theme');
|
||
themeToggle.classList.add('dark-theme');
|
||
header.classList.add('dark-theme');
|
||
installItems.forEach(item => item.classList.add('dark-theme'));
|
||
folderTree.addClass('jstree-default-dark');
|
||
themeToggle.textContent = 'Светлая тема';
|
||
localStorage.setItem('theme', 'dark');
|
||
}
|
||
}
|
||
|
||
$(document).ready(function () {
|
||
// Загружаем все папки и записи при инициализации
|
||
loadFolders();
|
||
loadInstalls(null);
|
||
|
||
// Инициализация дерева папок с настройками для темной темы
|
||
$('#folder-tree').jstree({
|
||
'core': {
|
||
'data': function (node, cb) {
|
||
$.getJSON(`${API_URL}/folders`, function (data) {
|
||
console.log("Loaded folders:", data);
|
||
allFolders = data; // Сохраняем все папки
|
||
// Сортируем папки по имени
|
||
const sortedFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
|
||
const treeData = [
|
||
{ id: "root", text: "Корень", parent: "#" }
|
||
].concat(sortedFolders.map(folder => ({
|
||
id: folder.id,
|
||
text: `${folder.name} <span class="folder-actions">` +
|
||
`<button onclick="editFolder(${folder.id}, '${folder.name}')">✏️</button>` +
|
||
`<button onclick="deleteFolder(${folder.id})">🗑️</button></span>`,
|
||
parent: folder.parent_id ? folder.parent_id : "root"
|
||
})));
|
||
cb(treeData);
|
||
|
||
// Автоматически выбираем папку "Несортированные" после загрузки дерева
|
||
const unsortedFolder = sortedFolders.find(f => f.name === 'Несортированные');
|
||
if (unsortedFolder) {
|
||
setTimeout(() => {
|
||
$('#folder-tree').jstree('select_node', unsortedFolder.id);
|
||
loadInstalls(unsortedFolder.id);
|
||
updateFolderActions();
|
||
}, 100);
|
||
}
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading folders:", textStatus, error);
|
||
});
|
||
},
|
||
'check_callback': true,
|
||
'themes': {
|
||
'variant': 'default-dark' // Устанавливаем темную тему по умолчанию для jstree
|
||
}
|
||
},
|
||
'plugins': ['dnd', 'html_data'],
|
||
'dnd': {
|
||
'is_draggable': true
|
||
}
|
||
}).on('select_node.jstree', function (e, data) {
|
||
selectedFolderId = data.node.id === "root" ? null : data.node.id;
|
||
loadInstalls(selectedFolderId);
|
||
updateFolderActions();
|
||
}).on('move_node.jstree', function (e, data) {
|
||
const movedNode = data.node;
|
||
const newParentId = movedNode.parent === '#' ? null : movedNode.parent;
|
||
saveFolderPosition(movedNode.id, newParentId);
|
||
});
|
||
|
||
// Обработчики для поиска и выбора папки
|
||
$('#search-input').on('input', function () {
|
||
performSearch();
|
||
});
|
||
|
||
$('#folder-select').on('change', function () {
|
||
performSearch();
|
||
});
|
||
|
||
// Drag-and-drop для записей
|
||
$('#installs-list').on('dragstart', '.install-item', function (e) {
|
||
e.originalEvent.dataTransfer.setData('text/plain', $(this).data('id'));
|
||
});
|
||
|
||
$('#folder-tree').on('dragover', function (e) {
|
||
e.preventDefault();
|
||
}).on('drop', function (e) {
|
||
e.preventDefault();
|
||
const installId = e.originalEvent.dataTransfer.getData('text');
|
||
let targetFolderId = $(e.target).closest('.jstree-node').attr('id');
|
||
if (installId && targetFolderId) {
|
||
targetFolderId = targetFolderId === "root" ? null : targetFolderId;
|
||
moveInstallToFolder(installId, targetFolderId);
|
||
}
|
||
});
|
||
});
|
||
|
||
function loadFolders() {
|
||
$.getJSON(`${API_URL}/folders`, function (data) {
|
||
// Сортируем папки по имени перед сохранением
|
||
allFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
|
||
updateFolderSelect(); // Обновляем выпадающий список
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading folders:", textStatus, error);
|
||
});
|
||
}
|
||
|
||
function loadInstalls(folderId) {
|
||
$.getJSON(`${API_URL}/installs`, function (data) {
|
||
console.log("Loaded installs:", data);
|
||
allInstalls = data;
|
||
let filtered = folderId ? data.filter(i => i.folder_id == folderId && i.folder_id !== null) : data;
|
||
displayInstalls(filtered);
|
||
updateNotesPanel();
|
||
}).fail(function(jqxhr, textStatus, error) {
|
||
console.error("Error loading installs:", textStatus, error);
|
||
$('#installs-list').html('<p>Ошибка загрузки данных. Проверьте консоль.</p>');
|
||
});
|
||
selectedFolderId = folderId;
|
||
}
|
||
|
||
function performSearch() {
|
||
const query = $('#search-input').val().toLowerCase().trim();
|
||
const folderId = $('#folder-select').val() || '';
|
||
|
||
console.log("Search query:", query, "Folder ID:", folderId);
|
||
|
||
let filteredInstalls = [...allInstalls];
|
||
|
||
if (folderId) {
|
||
filteredInstalls = filteredInstalls.filter(i =>
|
||
i.folder_id == folderId &&
|
||
allFolders.some(f => f.id === i.folder_id)
|
||
);
|
||
}
|
||
|
||
if (query) {
|
||
filteredInstalls = filteredInstalls.filter(i =>
|
||
i.rust_id.toLowerCase().trim().includes(query) ||
|
||
i.computer_name.toLowerCase().trim().includes(query)
|
||
);
|
||
}
|
||
|
||
sortInstalls(null, filteredInstalls);
|
||
}
|
||
|
||
function displayInstalls(installs) {
|
||
console.log("Displaying installs:", installs);
|
||
const isDarkTheme = document.body.classList.contains('dark-theme');
|
||
if (installs.length === 0) {
|
||
$('#installs-list').html('<p>Нет результатов</p>');
|
||
} else {
|
||
$('#installs-list').html(installs.map(item => `
|
||
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
|
||
<div class="computer-name">${item.computer_name} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')">✏️</button></div>
|
||
<div class="connection-id"><span class="protocol-icon">${protocolIcons[item.protocol]}</span><a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link ${isDarkTheme ? 'dark-theme' : ''}" target="_blank">${item.rust_id}</a> <button class="copy-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="copyToClipboard('${item.rust_id}')">📋</button> <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')">✏️</button></div>
|
||
<div class="install-time">${item.install_time} <button class="edit-button ${isDarkTheme ? 'dark-theme' : ''}" onclick="editField(${item.id}, 'install_time', '${item.install_time}')">✏️</button></div>
|
||
<div class="actions"><span class="action-buttons"><button onclick="deleteInstall(${item.id})">Удалить</button> <button onclick="openNoteModal(${item.id}, '${item.note}')">Заметка</button></span></div>
|
||
</div>
|
||
`).join(''));
|
||
}
|
||
updateSortArrows();
|
||
}
|
||
|
||
function selectInstall(installId, rustId, protocol) {
|
||
selectedInstallId = installId;
|
||
$('.install-item').removeClass('selected');
|
||
$(`#installs-list .install-item[data-id="${installId}"]`).addClass('selected');
|
||
updateNotesPanel();
|
||
const link = protocolLinks[protocol] + rustId;
|
||
window.location.href = link;
|
||
}
|
||
|
||
function sortInstalls(field, filteredInstalls = null) {
|
||
let installs = filteredInstalls || (selectedFolderId
|
||
? allInstalls.filter(i => i.folder_id == folderId && i.folder_id !== null)
|
||
: allInstalls);
|
||
|
||
if (field) {
|
||
if (sortField === field) {
|
||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
sortField = field;
|
||
sortDirection = 'asc';
|
||
}
|
||
installs.sort((a, b) => {
|
||
let valA = a[field], valB = b[field];
|
||
if (field === 'install_time') {
|
||
valA = new Date(valA);
|
||
valB = new Date(valB);
|
||
}
|
||
return sortDirection === 'asc' ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1);
|
||
});
|
||
}
|
||
displayInstalls(installs);
|
||
}
|
||
|
||
function updateSortArrows() {
|
||
$('.sort-arrow').text('');
|
||
if (sortField) {
|
||
$(`.header div:contains("${sortField === 'computer_name' ? 'Имя компьютера' : sortField === 'rust_id' ? 'ID подключения' : 'Время установки'}") .sort-arrow`)
|
||
.text(sortDirection === 'asc' ? '↑' : '↓');
|
||
}
|
||
}
|
||
|
||
function addFolder() {
|
||
const name = prompt("Введите имя папки:");
|
||
if (name) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders`,
|
||
type: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name, parent_id: selectedFolderId }),
|
||
success: function () {
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function editFolder(folderId, currentName) {
|
||
const newName = prompt("Введите новое имя папки:", currentName);
|
||
if (newName) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name: newName }),
|
||
success: function () {
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteFolder(folderId) {
|
||
if (confirm("Удалить папку? Все записи будут перемещены в корень.")) {
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'DELETE',
|
||
success: function () {
|
||
$.getJSON(`${API_URL}/installs`, function (data) {
|
||
const folderInstalls = data.filter(i => i.folder_id == folderId);
|
||
folderInstalls.forEach(item => {
|
||
moveInstallToFolder(item.id, null);
|
||
});
|
||
});
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 403) {
|
||
alert("Нельзя удалить папку 'Несортированные'.");
|
||
} else {
|
||
console.error("Ошибка удаления:", status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function addInstall() {
|
||
const rustId = $('#rustId').val();
|
||
const computerName = $('#computerName').val();
|
||
const installTime = $('#installTime').val() || '';
|
||
const protocol = $('#protocol').val();
|
||
const note = $('#note').val() || '';
|
||
|
||
if (installTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(installTime)) {
|
||
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
|
||
return;
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/install`,
|
||
type: 'POST',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ rust_id: rustId, computer_name: computerName, install_time: installTime, folder_id: selectedFolderId, protocol: protocol, note: note }),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 400) {
|
||
alert(xhr.responseJSON.detail);
|
||
} else {
|
||
console.error("Ошибка добавления:", status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function editField(installId, field, currentValue) {
|
||
let newValue;
|
||
if (field === 'install_time') {
|
||
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'} (опционально, формат для времени: ГГГГ-ММ-ДД ЧЧ:ММ:СС):`, currentValue || '');
|
||
if (newValue && field === 'install_time' && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(newValue)) {
|
||
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
|
||
return;
|
||
}
|
||
} else {
|
||
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'}:`, currentValue || '');
|
||
}
|
||
|
||
if (newValue !== null && newValue !== currentValue) {
|
||
let data = {};
|
||
data[field] = newValue;
|
||
if (field === 'install_time') {
|
||
data['folder_id'] = selectedFolderId;
|
||
}
|
||
// Добавляем текущее значение protocol
|
||
const install = allInstalls.find(i => i.id === installId);
|
||
if (install) {
|
||
data['protocol'] = install.protocol;
|
||
}
|
||
$.ajax({
|
||
url: `${API_URL}/install/${installId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify(data),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
if (xhr.status === 400) {
|
||
alert(xhr.responseJSON.detail);
|
||
} else {
|
||
console.error(`Ошибка редактирования ${field}:`, status, error);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function deleteInstall(id) {
|
||
if (confirm("Удалить запись?")) {
|
||
$.ajax({
|
||
url: `${API_URL}/install/${id}`,
|
||
type: 'DELETE',
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function moveInstallToFolder(installId, folderId) {
|
||
const install = allInstalls.find(i => i.id === installId);
|
||
if (install) {
|
||
$.ajax({
|
||
url: `${API_URL}/install/${installId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
folder_id: folderId,
|
||
protocol: install.protocol
|
||
}),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Ошибка переноса:", status, error);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function exportCSV() {
|
||
const folderId = $('#folder-select').val() || '';
|
||
window.location.href = `${API_URL}/export/csv?folder_id=${folderId}`;
|
||
}
|
||
|
||
function importCSV(file) {
|
||
if (!file) return;
|
||
|
||
const folderId = $('#folder-select').val() || '';
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
if (folderId) {
|
||
formData.append('folder_id', folderId);
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/import/csv`,
|
||
type: 'POST',
|
||
data: formData,
|
||
processData: false,
|
||
contentType: false,
|
||
success: function (response) {
|
||
alert(response.message);
|
||
loadInstalls(selectedFolderId);
|
||
},
|
||
error: function (xhr, status, error) {
|
||
alert(`Ошибка импорта: ${xhr.responseJSON.detail || error}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
function openNoteModal(installId, currentNote) {
|
||
selectedInstallId = installId;
|
||
$('#note-textarea').val(currentNote);
|
||
$('#note-modal').show();
|
||
$('#modal-overlay').show();
|
||
}
|
||
|
||
function saveNote() {
|
||
const note = $('#note-textarea').val() || '';
|
||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||
if (install) {
|
||
$.ajax({
|
||
url: `${API_URL}/install/${selectedInstallId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({
|
||
note: note,
|
||
protocol: install.protocol
|
||
}),
|
||
success: function () {
|
||
loadInstalls(selectedFolderId);
|
||
closeNoteModal();
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Ошибка сохранения заметки:", status, error);
|
||
alert("Не удалось сохранить заметку");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function closeNoteModal() {
|
||
$('#note-modal').hide();
|
||
$('#modal-overlay').hide();
|
||
$('#note-textarea').val('');
|
||
}
|
||
|
||
function updateNotesPanel() {
|
||
if (selectedInstallId) {
|
||
const install = allInstalls.find(i => i.id === selectedInstallId);
|
||
if (install && install.note) {
|
||
$('#notes-content').html(marked.parse(install.note));
|
||
} else {
|
||
$('#notes-content').html('<p>Нет заметок</p>');
|
||
}
|
||
} else {
|
||
$('#notes-content').html('<p>Выберите подключение для просмотра заметки</p>');
|
||
}
|
||
}
|
||
|
||
function updateFolderActions() {
|
||
$('.folder-actions').hide();
|
||
if (selectedFolderId) {
|
||
$(`#${selectedFolderId} .folder-actions`).show();
|
||
} else {
|
||
$('#root .folder-actions').show();
|
||
}
|
||
}
|
||
|
||
function updateFolderSelect() {
|
||
const $folderSelect = $('#folder-select');
|
||
$folderSelect.empty().append('<option value="">Все папки</option>');
|
||
allFolders.forEach(folder => {
|
||
$folderSelect.append(`<option value="${folder.id}">${folder.name}</option>`);
|
||
});
|
||
}
|
||
|
||
$('#installs-list').on('click', '.install-item', function () {
|
||
selectedInstallId = $(this).data('id');
|
||
$('.install-item').removeClass('selected');
|
||
$(this).addClass('selected');
|
||
updateNotesPanel();
|
||
});
|
||
|
||
function formatBold() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end);
|
||
const newText = `**${selected}**`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - 4;
|
||
}
|
||
|
||
function formatItalic() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end);
|
||
const newText = `*${selected}*`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - 2;
|
||
}
|
||
|
||
function formatLink() {
|
||
const textarea = $('#note-textarea');
|
||
const start = textarea[0].selectionStart;
|
||
const end = textarea[0].selectionEnd;
|
||
const text = textarea.val();
|
||
const selected = text.substring(start, end) || 'текст';
|
||
const url = prompt('Введите URL ссылки:', 'https://example.com');
|
||
if (url) {
|
||
const newText = `[${selected}](${url})`;
|
||
textarea.val(text.substring(0, start) + newText + text.substring(end));
|
||
textarea[0].selectionStart = start;
|
||
textarea[0].selectionEnd = start + newText.length - (selected ? 0 : '[текст]'.length);
|
||
}
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).catch(err => {
|
||
console.error('Ошибка копирования: ', err);
|
||
});
|
||
}
|
||
|
||
function saveFolderPosition(folderId, newParentId) {
|
||
const folder = allFolders.find(f => f.id == folderId);
|
||
if (!folder) {
|
||
console.error(`Folder with id ${folderId} not found`);
|
||
return;
|
||
}
|
||
|
||
$.ajax({
|
||
url: `${API_URL}/folders/${folderId}`,
|
||
type: 'PUT',
|
||
contentType: 'application/json',
|
||
data: JSON.stringify({ name: folder.name, parent_id: newParentId }),
|
||
success: function () {
|
||
console.log(`Folder ${folderId} moved to parent ${newParentId}`);
|
||
loadFolders();
|
||
$('#folder-tree').jstree(true).refresh();
|
||
},
|
||
error: function (xhr, status, error) {
|
||
console.error("Error saving folder position:", status, error);
|
||
console.error("Response:", xhr.responseText);
|
||
alert("Не удалось сохранить новое положение папки. Проверьте консоль.");
|
||
$('#folder-tree').jstree(true).refresh();
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |