Files
addressbook_web/templates/index.html
T
2025-03-10 14:35:45 +03:00

1205 lines
51 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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
height: 100vh;
background-color: #f4f4f4;
}
body.dark-theme {
background-color: #222;
color: #fff;
}
#tree-container, #installs-container, #notes-panel {
overflow: auto;
}
#tree-container {
width: 250px;
min-width: 200px;
border-right: 1px solid #ccc;
padding: 10px;
background-color: #fff;
}
#tree-container.dark-theme {
border-right-color: #444;
background-color: #333;
}
#installs-container {
flex-grow: 1;
padding: 10px;
background-color: #fff;
}
#installs-container.dark-theme {
background-color: #333;
}
#notes-panel {
width: 300px;
min-width: 200px;
border-left: 1px solid #ccc;
padding: 10px;
background-color: #fff;
}
#notes-panel.dark-theme {
border-left-color: #444;
background-color: #333;
}
#splitter {
width: 5px;
background-color: #ccc;
cursor: col-resize;
position: relative;
z-index: 10;
}
#splitter.dark-theme {
background-color: #444;
}
.header-logo {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.header-logo img {
height: 40px;
margin-right: 10px;
}
.theme-toggle, #add-record-button {
padding: 5px 10px;
cursor: pointer;
}
.theme-toggle.dark-theme, #add-record-button.dark-theme {
background-color: #555;
color: #fff;
border: 1px solid #777;
}
.export-import-container {
margin-bottom: 10px;
}
#search-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
#search-container.dark-theme input, #search-container.dark-theme select {
background-color: #444;
color: #fff;
border: 1px solid #666;
}
.header {
display: flex;
justify-content: space-between;
font-weight: bold;
padding: 5px;
background-color: #e0e0e0;
cursor: pointer;
}
.header.dark-theme {
background-color: #444;
}
.header div {
flex: 1;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.sort-arrow {
margin-left: 5px;
}
.install-item {
display: flex;
justify-content: space-between;
padding: 5px;
border-bottom: 1px solid #ddd;
cursor: pointer;
align-items: center;
}
.install-item.dark-theme {
border-bottom-color: #555;
background-color: #444;
}
.install-item:hover {
background-color: #f0f0f0;
}
.install-item.dark-theme:hover {
background-color: #555;
}
.install-item.selected {
background-color: #d0d0d0;
}
.install-item.dark-theme.selected {
background-color: #666;
}
.computer-name, .connection-id, .install-time, .actions {
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
.protocol-icon {
vertical-align: middle;
height: 16px;
width: 16px;
margin-right: 5px;
}
.connection-link {
color: #007bff;
text-decoration: none;
flex-grow: 1;
}
.connection-link.dark-theme {
color: #66b0ff;
}
.action-buttons button {
margin: 0 2px;
padding: 2px 5px;
}
.action-buttons.dark-theme button {
background-color: #555;
color: #fff;
border: 1px solid #777;
}
#notes-content {
padding: 10px;
min-height: 200px;
border: 1px solid #ccc;
}
#notes-content.dark-theme {
border-color: #444;
background-color: #444;
}
#modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
#add-modal, #note-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border-radius: 5px;
z-index: 1001;
width: 400px;
}
#add-modal.dark-theme, #note-modal.dark-theme {
background: #333;
color: #fff;
}
#add-modal input, #add-modal select, #add-modal textarea,
#note-modal textarea {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
#add-modal.dark-theme input, #add-modal.dark-theme select, #add-modal.dark-theme textarea,
#note-modal.dark-theme textarea {
background-color: #444;
color: #fff;
border: 1px solid #666;
}
.markdown-buttons, .image-upload {
margin-bottom: 10px;
}
.time-hint {
font-size: 0.8em;
color: #666;
}
.time-hint.dark-theme {
color: #bbb;
}
.status-online {
background-color: green;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
margin-left: 5px;
}
.status-offline {
background-color: red;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
margin-left: 5px;
}
.highlight {
background-color: #e0e0e0;
}
.highlight.dark-theme {
background-color: #555;
}
</style>
</head>
<body>
<div id="tree-container">
<h2>Папки</h2>
<div id="folder-tree"></div>
<button onclick="addFolder()">Добавить папку</button>
</div>
<div id="splitter"></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 class="export-import-container">
<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 id="search-container">
<input type="text" id="search-input" placeholder="Поиск по ID или имени">
<select id="folder-select">
<option value="">Все папки</option>
</select>
<button id="add-record-button" onclick="openAddModal()">Добавить запись</button>
</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="add-modal">
<h3>Добавить запись</h3>
<input type="text" id="add-rustId" placeholder="ID подключения" required>
<input type="text" id="add-computerName" placeholder="Имя компьютера" required>
<input type="text" id="add-installTime" placeholder="Время (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС)">
<select id="add-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="add-note" placeholder="Заметка (Markdown поддерживается)"></textarea>
<button onclick="addInstall()">Добавить</button>
<button onclick="closeAddModal()">Закрыть</button>
<div class="time-hint">Пример: 2025-03-05 14:30:00</div>
</div>
<div id="note-modal">
<h3>Редактировать заметку</h3>
<div class="markdown-buttons">
<button onclick="formatBold()">Жирный</button>
<button onclick="formatItalic()">Курсив</button>
<button onclick="formatLink()">Ссылка</button>
</div>
<div class="image-upload">
<input type="file" id="image-upload-input" accept="image/*" onchange="uploadImage()">
<button onclick="document.getElementById('image-upload-input').click()">Добавить изображение</button>
</div>
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
<button onclick="saveNote()">Сохранить</button>
<button onclick="closeNoteModal()">Закрыть</button>
</div>
<script>
const API_URL = "/api";
let selectedFolderId = null;
let allInstalls = [];
let allFolders = [];
let selectedInstallId = null;
let sortField = null;
let sortDirection = 'asc';
let ws = null;
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('add-modal').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.getElementById('splitter').classList.add('dark-theme');
document.querySelector('.theme-toggle').classList.add('dark-theme');
document.querySelector('#add-record-button').classList.add('dark-theme');
document.querySelectorAll('.install-item').forEach(item => item.classList.add('dark-theme'));
document.querySelector('.header').classList.add('dark-theme');
$('#folder-tree').addClass('jstree-default-dark');
}
$('#installs-list').on('click', '.action-buttons button:nth-child(2)', function () {
const installId = $(this).closest('.install-item').data('id');
const install = allInstalls.find(i => i.id === installId);
openNoteModal(installId, install?.note || '');
});
const splitter = document.getElementById('splitter');
const treeContainer = document.getElementById('tree-container');
let isResizing = false;
splitter.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const containerRect = document.body.getBoundingClientRect();
let newWidth = e.clientX - containerRect.left;
const minWidth = 200;
const maxWidth = containerRect.width * 0.5;
if (newWidth < minWidth) newWidth = minWidth;
if (newWidth > maxWidth) newWidth = maxWidth;
treeContainer.style.width = `${newWidth}px`;
});
document.addEventListener('mouseup', () => {
isResizing = false;
document.body.style.cursor = 'default';
});
document.addEventListener('keydown', (e) => {
if (!selectedFolderId || !allInstalls.length) return;
const items = $('#installs-list .install-item');
let currentIndex = items.index($('.install-item.selected'));
if (e.key === 'ArrowUp' && currentIndex > 0) {
e.preventDefault();
currentIndex--;
} else if (e.key === 'ArrowDown' && currentIndex < items.length - 1) {
e.preventDefault();
currentIndex++;
} else {
return;
}
selectedInstallId = $(items[currentIndex]).data('id');
$('.install-item').removeClass('selected');
$(items[currentIndex]).addClass('selected');
updateNotesPanel();
$(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
connectWebSocket();
});
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 addModal = document.getElementById('add-modal');
const noteModal = document.getElementById('note-modal');
const searchContainer = document.getElementById('search-container');
const importLabel = document.getElementById('import-label');
const splitter = document.getElementById('splitter');
const themeToggle = document.querySelector('.theme-toggle');
const addRecordButton = document.getElementById('add-record-button');
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');
addModal.classList.remove('dark-theme');
noteModal.classList.remove('dark-theme');
searchContainer.classList.remove('dark-theme');
importLabel.classList.remove('dark-theme');
splitter.classList.remove('dark-theme');
themeToggle.classList.remove('dark-theme');
addRecordButton.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');
addModal.classList.add('dark-theme');
noteModal.classList.add('dark-theme');
searchContainer.classList.add('dark-theme');
importLabel.classList.add('dark-theme');
splitter.classList.add('dark-theme');
themeToggle.classList.add('dark-theme');
addRecordButton.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'
}
},
'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);
}).on('dragover.jstree', function (e) {
e.preventDefault();
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
}
}).on('dragenter.jstree', function (e) {
e.preventDefault();
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
}
}).on('dragleave.jstree', function (e) {
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.removeClass('highlight');
}
}).on('drop.jstree', function (e) {
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.removeClass('highlight');
}
});
$('#search-input').on('input', function () {
performSearch();
});
$('#folder-select').on('change', function () {
performSearch();
});
$('#installs-list').on('dragstart', '.install-item', function (e) {
console.log('Drag start:', $(this).data('id'));
e.originalEvent.dataTransfer.setData('text/plain', $(this).data('id'));
});
$('#folder-tree').on('dragover', function (e) {
e.preventDefault();
console.log('Drag over');
}).on('drop', function (e) {
e.preventDefault();
console.log('Drop event triggered');
const installId = e.originalEvent.dataTransfer.getData('text');
console.log('Dropped installId:', installId);
let targetFolderId = $(e.target).closest('.jstree-node').attr('id');
console.log('Target folderId:', targetFolderId);
if (installId && targetFolderId) {
targetFolderId = targetFolderId === "root" ? null : targetFolderId;
moveInstallToFolder(installId, targetFolderId);
}
});
});
function connectWebSocket() {
if (ws) {
ws.close();
}
ws = new WebSocket(`wss://${window.location.host}/ws`);
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === "update") {
const updatedInstall = data.data;
const index = allInstalls.findIndex(i => i.id === updatedInstall.id);
if (index !== -1) {
allInstalls[index] = updatedInstall;
} else {
allInstalls.push(updatedInstall);
alert(`Новое подключение добавлено: ${updatedInstall.computer_name} (ID: ${updatedInstall.rust_id})`);
}
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onclose = function() {
console.log('WebSocket disconnected, retrying...');
let retryDelay = 1000; // Начальная задержка 1 секунда
const maxDelay = 30000; // Максимальная задержка 30 секунд
const maxAttempts = 10;
let attempts = 0;
function reconnect() {
if (attempts < maxAttempts) {
setTimeout(() => {
connectWebSocket();
retryDelay = Math.min(retryDelay * 2, maxDelay);
attempts++;
}, retryDelay);
} else {
console.error('Max reconnect attempts reached');
}
}
reconnect();
};
}
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();
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
}
}).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 => {
const lastSeen = new Date(item.last_seen || '1970-01-01');
const isOnline = (new Date() - lastSeen) < 60000; // Онлайн, если менее 1 минуты
const statusClass = isOnline ? 'status-online' : 'status-offline';
return `
<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><span class="${statusClass}"></span> <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 class="note-button">Заметка</button></span></div>
</div>
`;
}).join(''));
}
updateSortArrows();
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
}
}
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 == selectedFolderId && 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 openAddModal() {
$('#add-rustId').val('');
$('#add-computerName').val('');
$('#add-installTime').val('');
$('#add-protocol').val('rustdesk');
$('#add-note').val('');
$('#add-modal').show();
$('#modal-overlay').show();
}
function closeAddModal() {
$('#add-modal').hide();
$('#modal-overlay').hide();
}
function addInstall() {
const rustId = $('#add-rustId').val();
const computerName = $('#add-computerName').val();
const installTime = $('#add-installTime').val() || '';
const protocol = $('#add-protocol').val();
const note = $('#add-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);
closeAddModal();
},
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;
}
const install = allInstalls.find(i => i.id === installId);
if (install) {
data['protocol'] = install.protocol;
data['note'] = install.note;
}
$.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) {
console.log('Moving installId:', installId, 'to folderId:', folderId);
const install = allInstalls.find(i => i.id === parseInt(installId));
if (!install) {
console.error('Install not found:', installId);
return;
}
$.ajax({
url: `${API_URL}/install/${installId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
rust_id: install.rust_id,
computer_name: install.computer_name,
install_time: install.install_time,
folder_id: folderId,
protocol: install.protocol,
note: install.note
}),
success: function () {
console.log('Move successful');
loadInstalls(selectedFolderId);
},
error: function (xhr, status, error) {
console.error("Ошибка переноса:", status, error);
if (xhr.status === 404) {
alert("Запись не найдена");
} else {
alert("Ошибка при перемещении записи");
}
}
});
}
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) {
console.log('Opening note modal for installId:', installId, 'with note:', currentNote);
selectedInstallId = installId;
$('#note-textarea').val(currentNote || '');
$('#note-modal').show();
$('#modal-overlay').show();
document.getElementById('image-upload-input').value = '';
}
function uploadImage() {
const fileInput = document.getElementById('image-upload-input');
const file = fileInput.files[0];
if (!file || !selectedInstallId) {
alert('Пожалуйста, выберите запись и изображение.');
return;
}
const formData = new FormData();
formData.append('install_id', selectedInstallId);
formData.append('file', file);
$.ajax({
url: `${API_URL}/upload-image`,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
console.log('Image uploaded:', response);
const imageUrl = response.url;
const textarea = $('#note-textarea');
const cursorPos = textarea[0].selectionStart;
const text = textarea.val();
const altText = prompt('Введите альтернативный текст для изображения:', file.name);
const markdownImage = `![${altText || 'image'}](${imageUrl})`;
textarea.val(text.substring(0, cursorPos) + markdownImage + text.substring(cursorPos));
textarea[0].selectionStart = cursorPos + markdownImage.length;
textarea[0].selectionEnd = cursorPos + markdownImage.length;
},
error: function (xhr, status, error) {
console.error('Error uploading image:', status, error);
alert(`Не удалось загрузить изображение: ${xhr.responseJSON?.detail || 'Неизвестная ошибка'}`);
}
});
}
function saveNote() {
console.log('Saving note for installId:', selectedInstallId);
if (!selectedInstallId) {
console.error('No install selected');
alert('Выберите запись для редактирования заметки.');
return;
}
const note = $('#note-textarea').val() || '';
const install = allInstalls.find(i => i.id === selectedInstallId);
if (install) {
const currentSelectedId = selectedInstallId;
$.ajax({
url: `${API_URL}/install/${selectedInstallId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
rust_id: install.rust_id,
computer_name: install.computer_name,
install_time: install.install_time,
folder_id: install.folder_id,
protocol: install.protocol,
note: note
}),
success: function () {
console.log('Note saved successfully');
loadInstalls(selectedFolderId);
selectedInstallId = currentSelectedId;
closeNoteModal();
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel();
},
error: function (xhr, status, error) {
console.error("Ошибка сохранения заметки:", status, error);
alert(`Не удалось сохранить заметку: ${xhr.responseJSON?.detail || error}`);
}
});
}
}
function closeNoteModal() {
$('#note-modal').hide();
$('#modal-overlay').hide();
$('#note-textarea').val('');
document.getElementById('image-upload-input').value = '';
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel();
}
}
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;
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();
}
});
}
function startPeriodicSync() {
setInterval(() => {
$.getJSON(`${API_URL}/installs`, function (data) {
allInstalls = data;
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
}).fail(function(jqxhr, textStatus, error) {
console.error("Error syncing installs:", textStatus, error);
});
}, 60000); // Каждые 60 секунд
}
startPeriodicSync();
</script>
</body>
</html>