Files
addressbook_web/templates/index.html
T
2025-03-05 12:45:23 +03:00

580 lines
29 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; }
#tree-container { width: 20%; padding: 10px; border-right: 1px solid #ccc; }
#installs-container { width: 50%; padding: 10px; position: relative; }
.install-item { padding: 5px; margin: 2px; border: 1px solid #ddd; cursor: move; position: relative; }
.form-container { margin-bottom: 20px; }
.jstree-node { position: relative; }
.folder-actions { display: none; margin-left: 10px; }
.jstree-node:hover .folder-actions { display: inline; }
#search-container { position: absolute; top: 10px; right: 10px; text-align: right; }
#search-input { width: 200px; }
#installs-list { margin-top: 20px; }
.header { display: flex; justify-content: space-between; font-weight: bold; padding: 5px; background: #f0f0f0; cursor: pointer; }
.header div { flex: 1; text-align: center; }
.header div:hover { background: #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; }
.edit-button { display: none; position: absolute; right: 5px; top: 50%; transform: translateY(-50%); background: #007bff; color: white; border: none; padding: 2px 5px; border-radius: 3px; cursor: pointer; }
.edit-button:hover { background: #0056b3; }
.install-item:hover .edit-button { display: inline-block; }
.action-buttons { display: none; }
.install-item:hover .action-buttons { display: inline; }
#import-form { margin-top: 10px; }
#import-file { display: none; }
#import-label { cursor: pointer; background: #007bff; color: white; padding: 5px 10px; border-radius: 5px; }
#import-label:hover { background: #0056b3; }
.folder-select { margin: 5px 0; }
.time-hint { font-size: 12px; color: #666; margin-top: 5px; }
#notes-panel { width: 30%; padding: 10px; border-left: 1px solid #ccc; background: #f9f9f9; min-height: 200px; margin-top: 70px; } /* Смещаем ниже и начинаем с уровня заголовка */
#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 .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 button { margin-right: 10px; }
#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; } /* Логотип 50x50 пикселей */
</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="АйТи-Депо логотип"> <!-- Логотип -->
Органайзер АйТи-Депо
</h1>
<div id="search-container">
<input type="text" id="search-input" placeholder="Поиск по ID или имени" onkeyup="filterInstalls()">
<div>
<input type="radio" id="search-current" name="search-scope" value="current" checked>
<label for="search-current">Текущая папка</label>
<input type="radio" id="search-all" name="search-scope" value="all">
<label for="search-all">Все папки</label>
</div>
<div style="margin-top: 10px;">
<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>
<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('rust_id')">ID подключения<span class="sort-arrow"></span></div>
<div onclick="sortInstalls('computer_name')">Имя компьютера<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";
let selectedFolderId = null;
let allInstalls = [];
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).ready(function () {
// Получаем ID папки "Несортированные" перед инициализацией дерева
$.getJSON(`${API_URL}/folders`, function (folders) {
const unsortedFolder = folders.find(f => f.name === 'Несортированные');
const unsortedFolderId = unsortedFolder ? unsortedFolder.id : null;
// Заполняем выпадающий список папок
const $folderSelect = $('#folder-select');
$folderSelect.append('<option value="">Все папки</option>');
folders.forEach(folder => {
$folderSelect.append(`<option value="${folder.id}">${folder.name}</option>`);
});
// Инициализация дерева папок
$('#folder-tree').jstree({
'core': {
'data': function (node, cb) {
$.getJSON(`${API_URL}/folders`, function (data) {
const treeData = [
{ id: "root", text: "Корень", parent: "#" }
].concat(data.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);
});
},
'check_callback': true
},
'plugins': ['dnd', 'html_data']
}).on('select_node.jstree', function (e, data) {
selectedFolderId = data.node.id === "root" ? null : data.node.id;
loadInstalls(selectedFolderId);
});
// Автоматически выбираем "Несортированные" при загрузке
if (unsortedFolderId) {
setTimeout(() => {
$('#folder-tree').jstree('select_node', unsortedFolderId);
loadInstalls(unsortedFolderId);
}, 100); // Небольшая задержка, чтобы дерево инициализировалось
} else {
loadInstalls(null);
$('#folder-tree').jstree('select_node', 'root');
}
// 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 loadInstalls(folderId) {
$.getJSON(`${API_URL}/installs`, function (data) {
allInstalls = data;
let filtered = folderId ? data.filter(i => i.folder_id == folderId) : data.filter(i => i.folder_id === null);
displayInstalls(filtered);
updateNotesPanel(); // Обновляем заметки после загрузки
});
selectedFolderId = folderId;
}
function displayInstalls(installs) {
$('#installs-list').html(installs.map(item => `
<div class="install-item" data-id="${item.id}" draggable="true" style="display: flex; justify-content: space-between; position: relative;">
<div style="flex: 1; text-align: center; position: relative;">
<span class="protocol-icon">${protocolIcons[item.protocol]}</span>
<a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link" onclick="openRemote('${item.rust_id}', '${item.protocol}'); return false;">${item.rust_id}</a>
<button class="edit-button" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')">✏️</button>
</div>
<div style="flex: 1; text-align: center; position: relative;">
${item.computer_name}
<button class="edit-button" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')">✏️</button>
</div>
<div style="flex: 1; text-align: center; position: relative;">
${item.install_time}
<button class="edit-button" onclick="editField(${item.id}, 'install_time', '${item.install_time}')">✏️</button>
</div>
<div style="flex: 1; text-align: center; position: relative;">
<span class="action-buttons">
<button onclick="deleteInstall(${item.id})">Удалить</button>
<button onclick="openNoteModal(${item.id}, '${item.note}')">Заметка</button>
</span>
</div>
</div>
`).join(''));
updateSortArrows();
}
function filterInstalls() {
const query = $('#search-input').val().toLowerCase();
const scope = $('input[name="search-scope"]:checked').val();
let filtered = scope === 'current'
? (selectedFolderId ? allInstalls.filter(i => i.folder_id == selectedFolderId) : allInstalls.filter(i => i.folder_id === null))
: allInstalls;
filtered = filtered.filter(i =>
i.rust_id.toLowerCase().includes(query) ||
i.computer_name.toLowerCase().includes(query)
);
sortInstalls(null, filtered);
}
function sortInstalls(field, filteredInstalls = null) {
let installs = filteredInstalls || (selectedFolderId
? allInstalls.filter(i => i.folder_id == selectedFolderId)
: allInstalls.filter(i => i.folder_id === null));
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 === 'rust_id' ? 'ID подключения' : sortField === 'computer_name' ? 'Имя компьютера' : 'Время установки'}") .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 () {
$('#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 () {
$('#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);
});
});
$('#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 === 'rust_id' ? 'ID подключения' : field === 'computer_name' ? 'Имя компьютера' : 'Время установки'} (опционально, формат для времени: ГГГГ-ММ-ДД ЧЧ:ММ:СС):`, 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 === 'rust_id' ? 'ID подключения' : field === 'computer_name' ? 'Имя компьютера' : 'Время установки'}:`, currentValue || '');
}
if (newValue !== null && newValue !== currentValue) { // Проверяем, что значение изменилось
let data = {};
data[field] = newValue;
if (field === 'install_time') {
data['folder_id'] = selectedFolderId; // Добавляем folder_id для обновления
}
$.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 openRemote(id, protocol) {
if (confirm(`Подключиться к ${id} через ${protocol}?`)) {
const link = protocolLinks[protocol] + id;
window.location.href = link;
}
}
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)); // Рендеринг Markdown
} else {
$('#notes-content').html('<p>Нет заметок</p>');
}
} else {
$('#notes-content').html('<p>Выберите подключение для просмотра заметки</p>');
}
}
// Обновляем заметки при клике на подключение
$('#installs-list').on('click', '.install-item', function () {
selectedInstallId = $(this).data('id');
updateNotesPanel();
});
// Функции для форматирования Markdown
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); // Учитываем длину [текст](URL)
}
}
</script>
</body>
</html>