addressbook_web/templates/index.html

930 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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;
}
.install-item.dark-theme { background: #2a2a2a; }
.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;
}
#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;
}
.header.dark-theme { background: #333; }
.header > div {
display: table-cell;
padding: 5px;
text-align: center;
font-weight: bold;
}
.header > div:hover { background: #e0e0e0; }
.header > div.dark-theme:hover { background: #444; }
.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 = 'Светлая тема';
}
});
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');
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');
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');
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
},
'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);
if (installs.length === 0) {
$('#installs-list').html('<p>Нет результатов</p>');
} else {
$('#installs-list').html(installs.map(item => `
<div class="install-item" data-id="${item.id}" draggable="true">
<div class="computer-name">${item.computer_name} <button class="edit-button" 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" target="_blank">${item.rust_id}</a> <button class="copy-button" onclick="copyToClipboard('${item.rust_id}')">📋</button> <button class="edit-button" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')">✏️</button></div>
<div class="install-time">${item.install_time} <button class="edit-button" 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;
}
$.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) {
$.ajax({
url: `${API_URL}/install/${installId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ folder_id: folderId }),
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() || '';
$.ajax({
url: `${API_URL}/install/${selectedInstallId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ note: note }),
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>