rasskazhi-o-sebe-kto-ty-chto/index.html
2026-09-10 08:44:33 +00:00

1974 lines
110 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Инфраструктура - Дашборд</title>
<style>
* { margin: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--bg-primary, #0f1218);
color: var(--text-primary, #e4e4e7);
min-height: 100vh;
transition: background 0.3s, color 0.3s;
}
:root.light {
--bg-primary: #f4f4f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e4e4e7;
--text-primary: #18181b;
--text-secondary: #3f3f46;
--border: #d4d4d8;
--accent: #0891b2;
--accent-text: #ffffff;
--hover: #e4e4e7;
--active: #0891b2;
--active-text: #ffffff;
--stat-bg: #e4e4e7;
--stat-num: #0891b2;
--stat-label: #52525b;
--card-border: #d4d4d8;
--danger: #dc2626;
--success: #16a34a;
}
:root {
--bg-primary: #0f1218;
--bg-secondary: #171719;
--bg-tertiary: #27272a;
--text-primary: #e4e4e7;
--text-secondary: #a1a1aa;
--border: #3f3f46;
--accent: #00b8cc;
--accent-text: #0f1218;
--hover: #3f3f46;
--active: #00b8cc;
--active-text: #0f1218;
--stat-bg: #27272a;
--stat-num: #00b8cc;
--stat-label: #a1a1aa;
--card-border: #27272a;
--danger: #dc2626;
--success: #16a34a;
}
.login-screen {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-primary);
z-index: 2000;
align-items: center;
justify-content: center;
}
.login-screen.on { display: flex; }
.login-box {
background: var(--bg-secondary);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 32px;
width: 100%;
max-width: 400px;
}
.login-box h1 { font-size: 24px; margin-bottom: 24px; text-align: center; color: var(--text-primary); }
.login-box input { margin-bottom: 16px; }
.login-box button { width: 100%; }
.login-error { color: var(--danger); margin-bottom: 16px; display: none; }
.nav {
background: var(--bg-secondary);
padding: 12px 24px;
display: flex;
gap: 12px;
flex-wrap: wrap;
border-bottom: 1px solid var(--card-border);
position: sticky;
top: 0;
z-index: 100;
}
.nav button {
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.nav button:hover { background: var(--hover); }
.nav button.active { background: var(--active); border-color: var(--active); color: var(--active-text); }
.section { display: none; padding: 24px; }
.section.on { display: block; }
h1 { font-size: 28px; margin-bottom: 20px; color: var(--text-primary); }
h2 { font-size: 20px; margin: 24px 0 12px; color: var(--text-primary); }
.card {
background: var(--bg-secondary);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat {
background: var(--stat-bg);
padding: 16px;
border-radius: 10px;
text-align: center;
}
.stat .num { font-size: 32px; font-weight: bold; color: var(--stat-num); }
.stat .lbl { font-size: 13px; color: var(--stat-label); margin-top: 4px; }
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
input, select, textarea {
width: 100%;
padding: 10px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent);
}
label { font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px; }
button.btn {
background: var(--accent);
border: none;
color: var(--accent-text);
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
button.btn:hover { filter: brightness(1.1); }
button.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border);
}
button.btn-danger {
background: var(--danger);
color: #fff;
}
button.btn-sm {
padding: 6px 12px;
font-size: 13px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
}
th, td {
text-align: left;
padding: 12px;
border-bottom: 1px solid var(--card-border);
}
th {
background: var(--bg-tertiary);
font-weight: 600;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
user-select: none;
}
th:hover { background: var(--hover); }
tr:hover { background: var(--bg-tertiary); }
.actions { display: flex; gap: 8px; }
.priority-high { color: var(--danger); font-weight: 600; }
.priority-medium { color: #f59e0b; }
.priority-low { color: var(--success); }
.completed { opacity: 0.5; text-decoration: line-through; }
.link { color: var(--accent); text-decoration: none; }
.link:hover { text-decoration: underline; }
.search-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.search-row input { max-width: 300px; }
.modal {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.on { display: flex; }
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 24px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
color: var(--text-primary);
}
.modal-title { font-size: 20px; margin-bottom: 16px; color: var(--text-primary); }
.modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: flex-end;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.badge-high { background: #dc2626; color: #fff; }
.badge-medium { background: #f59e0b; color: #0f1218; }
.badge-low { background: #22c55e; color: #0f1218; }
.report-filters {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
</style>
</head>
<body>
<div class="nav" id="main-nav" style="display: none;">
<button data-section="dashboard" class="active">Дашборд</button>
<button data-section="todos">Список дел</button>
<button data-section="systems">Системы</button>
<button data-section="servers">Серверы</button>
<button data-section="admins">Админы</button>
<button data-section="users">Операторы</button>
<button data-section="distribution">Распределение</button>
<button data-section="app-users" id="nav-app-users" style="display: none;">Пользователи приложения</button>
<button data-section="roles" id="nav-roles" style="display: none;">Роли</button>
<button data-section="audit" id="nav-audit" style="display: none;">Лог аудита</button>
<button id="btn-update-perms" onclick="updatePermissions()" style="display:none;">🔄 Обновить права</button>
<button data-section="profile" id="nav-profile">👤 Кабинет</button>
<button id="btn-theme" style="margin-left: auto;" onclick="toggleTheme()">🌙</button>
<button id="user-info" style="margin-left: 8px;"></button>
<button data-section="logout" style="background: var(--danger);">Выход</button>
</div>
<!-- DASHBOARD -->
<div id="dashboard" class="section on">
<h1>Дашборд</h1>
<div class="stats">
<div class="stat" style="cursor:pointer;" onclick="navigateTo('todos')"><div class="num" id="stat-todos">0</div><div class="lbl">Задач всего</div></div>
<div class="stat" style="cursor:pointer;" onclick="navigateTo('todos')"><div class="num" id="stat-todos-high">0</div><div class="lbl">Высокий приоритет</div></div>
<div class="stat" style="cursor:pointer;" onclick="navigateTo('systems')"><div class="num" id="stat-systems">0</div><div class="lbl">Систем</div></div>
<div class="stat" style="cursor:pointer;" onclick="navigateTo('servers')"><div class="num" id="stat-servers">0</div><div class="lbl">Серверов</div></div>
<div class="stat" style="cursor:pointer;" onclick="navigateTo('admins')"><div class="num" id="stat-admins">0</div><div class="lbl">Админов</div></div>
<div class="stat" style="cursor:pointer;" onclick="navigateTo('users')"><div class="num" id="stat-users">0</div><div class="lbl">Пользователей</div></div>
</div>
<div class="card">
<h2>Распределение по админам</h2>
<div id="dashboard-dist"></div>
</div>
</div>
<!-- TODOS -->
<div id="todos" class="section">
<h1>Список дел</h1>
<div style="display: flex; gap: 16px; margin-bottom: 16px;">
<button class="btn btn-secondary" onclick="swapTodoBlocks()">🔄 Поменять блоки местами</button>
</div>
<!-- Общие задачи -->
<div id="common-todos-block">
<h2>Общие задачи</h2>
<p style="color: #71717a; margin-bottom: 16px;">Задачи создаёт администратор. Каждый пользователь отмечает выполнение только у себя.</p>
<button class="btn" id="btn-add-common-todo" onclick="openCommonTodoModal()">+ Добавить общую задачу</button>
<div class="search-row">
<input type="text" id="common-todo-search" placeholder="Поиск..." oninput="renderCommonTodos()">
</div>
<table>
<thead>
<tr>
<th>Задача</th>
<th>Приоритет</th>
<th>Срок</th>
<th>Автор</th>
<th>Статус у меня</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="common-todos-body"></tbody>
</table>
</div>
<!-- Личные задачи -->
<div id="personal-todos-block" style="margin-top: 32px;">
<h2>Личные задачи</h2>
<p style="color: #71717a; margin-bottom: 16px;">Ваши персональные задачи, видны только вам.</p>
<button class="btn" id="btn-add-personal-todo" onclick="openPersonalTodoModal()">+ Добавить личную задачу</button>
<div class="search-row">
<input type="text" id="personal-todo-search" placeholder="Поиск..." oninput="renderPersonalTodos()">
<select id="personal-todo-filter" onchange="renderPersonalTodos()">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="completed">Выполненные</option>
</select>
</div>
<table>
<thead>
<tr>
<th>Задача</th>
<th>Приоритет</th>
<th>Срок</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="personal-todos-body"></tbody>
</table>
</div>
</div>
<!-- SYSTEMS -->
<div id="systems" class="section">
<h1>Системы</h1>
<button class="btn" id="btn-add-system" onclick="openSystemModal()">+ Добавить систему</button>
<div class="search-row">
<input type="text" id="system-search" placeholder="Поиск..." oninput="renderSystems()">
</div>
<table>
<thead>
<tr>
<th>Система</th>
<th>Адрес</th>
<th>Описание</th>
<th>Ответственный</th>
<th>Техподдержка</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="systems-body"></tbody>
</table>
</div>
<!-- SERVERS -->
<div id="servers" class="section">
<h1>Серверы</h1>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<button class="btn" id="btn-add-servers" onclick="openServerModal()">+ Добавить сервер</button>
<button class="btn btn-secondary" id="btn-import-servers" onclick="importServers()">📥 Импорт из файл</button>
</div>
<div class="search-row">
<input type="text" id="server-search" placeholder="Поиск..." oninput="renderServers()">
</div>
<table>
<thead>
<tr>
<th data-sort="city">Город</th>
<th data-sort="region">Регион</th>
<th data-sort="cityCode">Код города</th>
<th data-sort="ip">IP</th>
<th>Описание</th>
</tr>
</thead>
<tbody id="servers-body"></tbody>
</table>
</div>
<!-- ADMINS -->
<div id="admins" class="section">
<h1>Коллеги-админы</h1>
<div style="display:flex;gap:8px;margin-bottom:12px;">
<button class="btn" id="btn-add-admins" onclick="openAdminModal()">+ Добавить админа</button>
<button class="btn btn-secondary" id="btn-import-admins" onclick="importAdmins()">📥 Импорт (старый)</button>
<button class="btn btn-secondary" id="btn-import-admins-1" onclick="importAdmins1()">📥 Импорт (обновление)</button>
</div>
<div class="search-row">
<input type="text" id="admin-search" placeholder="Поиск..." oninput="renderAdmins()">
</div>
<table>
<thead>
<tr>
<th data-sort="name">ФИО</th>
<th data-sort="position">Должность</th>
<th>Рабочий тел</th>
<th>Домашний тел</th>
<th>Сотовый</th>
<th data-sort="email">Email</th>
<th>Серверы</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="admins-body"></tbody>
</table>
</div>
<!-- USERS -->
<div id="users" class="section">
<h1>Операторы</h1>
<button class="btn" id="btn-add-users" onclick="openUserModal()">+ Добавить пользователя</button>
<div class="search-row">
<input type="text" id="user-search" placeholder="Поиск..." oninput="renderUsers()">
</div>
<table>
<thead>
<tr>
<th>ФИО</th>
<th>Логин</th>
<th>Система</th>
<th>Основание</th>
<th>Режим</th>
<th>Доп.</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="users-body"></tbody>
</table>
</div>
<!-- DISTRIBUTION -->
<div id="distribution" class="section">
<h1>Распределение систем по админам</h1>
<p style="color: #71717a; margin-bottom: 16px;">Данные автоматически берутся из вкладки <strong>Коллеги-админы</strong>. Добавляйте серверы там.</p>
<div class="search-row">
<input type="text" id="dist-search" placeholder="Поиск..." oninput="renderDist()">
</div>
<table>
<thead>
<tr>
<th onclick="sortDist('admin')" style="cursor: pointer;">Админ <span id="sort-admin">↕</span></th>
<th onclick="sortDist('city')" style="cursor: pointer;">Город <span id="sort-city">↕</span></th>
<th onclick="sortDist('cityCode')" style="cursor: pointer;">Код города <span id="sort-cityCode">↕</span></th>
<th onclick="sortDist('ip')" style="cursor: pointer;">IP сервера <span id="sort-ip">↕</span></th>
<th>Действия</th>
</tr>
</thead>
<tbody id="dist-body"></tbody>
</table>
</div>
<!-- APP USERS -->
<div id="app-users" class="section">
<h1>Пользователи приложения</h1>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<button class="btn" onclick="openAppUserModal()">+ Добавить пользователя</button>
<button class="btn btn-secondary" onclick="syncAdminsToUsers()">🔄 Добавить админов как пользователей</button>
</div>
<p style="color: #71717a; margin-bottom: 16px;">Кнопка создаст пользователей приложения для всех админов из вкладки <strong>Коллеги-админы</strong> с правами оператора по умолчанию.</p>
<div class="search-row">
<input type="text" id="appuser-search" placeholder="Поиск..." oninput="renderAppUsers()">
</div>
<table>
<thead>
<tr>
<th>Логин</th>
<th>ФИО</th>
<th>Должность</th>
<th>Телефон</th>
<th>Email</th>
<th>Город</th>
<th>Код города</th>
<th>IP адрес</th>
<th>Роль</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="app-users-body"></tbody>
</table>
</div>
<!-- ROLES -->
<div id="roles" class="section">
<h1>Роли</h1>
<div class="search-row">
<button class="btn btn-primary" onclick="addRole()">+ Добавить роль</button>
</div>
<table>
<thead>
<tr>
<th>Название</th>
<th>Описание</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="roles-body"></tbody>
</table>
</div>
<!-- AUDIT LOG -->
<div id="audit" class="section">
<h1>Лог аудита</h1>
<div class="search-row">
<input type="text" id="audit-search" placeholder="Поиск..." oninput="renderAudit()">
<select id="audit-user" onchange="renderAudit()">
<option value="">Все пользователи</option>
</select>
</div>
<table>
<thead>
<tr>
<th>Время</th>
<th>Пользователь</th>
<th>Действие</th>
<th>Детали</th>
</tr>
</thead>
<tbody id="audit-body"></tbody>
</table>
</div>
<!-- PROFILE -->
<div id="profile" class="section">
<h1>Личный кабинет</h1>
<div class="card" style="max-width: 800px;">
<div class="form-row">
<div><label>Логин *</label><input id="profile-login" readonly style="background: var(--bg-secondary); cursor: not-allowed;"></div>
<div><label>ФИО *</label><input id="profile-name"></div>
</div>
<div class="form-row">
<div><label>Должность *</label><input id="profile-position"></div>
<div><label>Почта</label><input id="profile-email" type="email"></div>
</div>
<div class="form-row">
<div><label>Рабочий телефон</label><input id="profile-workphone"></div>
<div><label>Домашний телефон</label><input id="profile-homephone"></div>
<div><label>Сотовый телефон</label><input id="profile-mobilephone"></div>
</div>
<div class="form-row">
<div><label>Город</label><input id="profile-city"></div>
<div><label>IP адрес</label><input id="profile-ipaddress"></div>
</div>
<div class="form-row">
<div><label>Пароль *</label><input id="profile-password" type="password"></div>
</div>
<div style="margin-top: 20px;">
<button class="btn" onclick="saveProfile()">💾 Сохранить</button>
</div>
</div>
</div>
<!-- LOGIN SCREEN -->
<div id="login-screen" class="login-screen">
<div class="login-box">
<h1>Вход в систему</h1>
<div class="login-error" id="login-error"></div>
<div class="form-row" style="display: block;">
<label>Логин</label>
<input id="login-username" placeholder="Введите логин">
</div>
<div class="form-row" style="display: block;">
<label>Пароль</label>
<input type="password" id="login-password" placeholder="Введите пароль">
</div>
<button class="btn" onclick="doLogin()">Войти</button>
</div>
</div>
<!-- MODALS -->
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-title" id="modal-title">Заголовок</div>
<div id="modal-body"></div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button class="btn" id="modal-save">Сохранить</button>
</div>
</div>
</div>
<script>
let commonTodos = [], personalTodos = [], systems = [], servers = [], admins = [], users = [], distribution = [], adminServers = [];
let todoBlocksSwapped = false;
let distSort = { field: 'admin', dir: 1 }; // 1 = asc, -1 = desc
let appUsers = [], auditLog = [], roles = [];
let sortField = null, sortDesc = false;
let sortFieldAdmin = null, sortDescAdmin = false;
let editId = null;
let currentAdminId = null;
let currentUser = null;
// Auth check
function checkAuth() {
const saved = localStorage.getItem('currentUser');
if (saved) {
try {
currentUser = JSON.parse(saved);
showMainApp();
} catch(e) {
console.error('Failed to parse currentUser:', e);
localStorage.removeItem('currentUser');
showLogin();
}
} else {
showLogin();
}
}
function showLogin() {
document.getElementById('login-screen').classList.add('on');
document.getElementById('main-nav').style.display = 'none';
document.querySelectorAll('.section').forEach(s => s.classList.remove('on'));
}
function showMainApp() {
console.log('showMainApp called, currentUser:', currentUser);
document.getElementById('login-screen').classList.remove('on');
document.getElementById('main-nav').style.display = 'flex';
document.getElementById('user-info').textContent = currentUser.name + ' (' + (currentUser.role === 'admin' ? 'Админ' : currentUser.role === 'moderator' ? 'Модератор' : 'Оператор') + ')';
document.getElementById('nav-app-users').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('nav-roles').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('nav-audit').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('btn-update-perms').style.display = currentUser.role === 'admin' ? '' : 'none';
const theme = localStorage.getItem('theme') || 'dark';
if (theme === 'light') {
document.documentElement.classList.add('light');
document.getElementById('btn-theme').textContent = '☀️';
}
console.log('Calling checkNavAccess');
checkNavAccess();
document.querySelectorAll('.section').forEach(s => s.classList.remove('on'));
document.getElementById('dashboard').classList.add('on');
loadAll();
}
function navigateTo(section) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('on'));
document.getElementById(section).classList.add('on');
document.querySelectorAll('.nav button').forEach(b => b.classList.remove('active'));
const navBtn = document.querySelector('.nav button[data-section="' + section + '"]');
if (navBtn) navBtn.classList.add('active');
}
async function doLogin() {
const login = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
const err = document.getElementById('login-error');
err.style.display = 'none';
try {
const res = await fetch('api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login, password })
});
const data = await res.json();
if (data.ok) {
currentUser = data.user;
localStorage.setItem('currentUser', JSON.stringify(currentUser));
showMainApp();
} else {
err.textContent = data.error || 'Ошибка входа';
err.style.display = 'block';
}
} catch (e) {
console.error('Login error:', e);
err.textContent = 'Ошибка подключения: ' + e.message;
err.style.display = 'block';
}
}
function doLogout() {
localStorage.removeItem('currentUser');
currentUser = null;
showLogin();
}
function togglePermissions() {
const role = document.getElementById('m-role').value;
document.getElementById('permissions-box').style.display = role === 'moderator' ? 'block' : 'none';
}
function hasPermission(section, action) {
if (!currentUser) return false;
const role = currentUser.role;
// Admin — полный доступ
if (role === 'admin') return true;
// Оператор — только просмотр на отдельные вкладки + личные задачи
if (role === 'operator') {
if (action === 'view') {
const viewSections = ['dashboard', 'systems', 'servers', 'admins', 'operators', 'distribution'];
return viewSections.includes(section);
}
if (action === 'create' && section === 'personal-todos') return true;
if (action === 'complete') return true; // закрывать свои задачи
return false;
}
// Модератор — все вкладки кроме appUsers + гибкие права
if (role === 'moderator') {
const perm = currentUser.permissions || {};
// Доступ к вкладке "Пользователи приложения" запрещён
if (section === 'appUsers' || section === 'users') return false;
const p = perm[section] || {};
if (action === 'view') return p.view === true;
return p[action] === true;
}
return false;
}
function checkNavAccess() {
const canTodosView = hasPermission('todos', 'view');
const canSystemsView = hasPermission('systems', 'view');
const canSystemsEdit = hasPermission('systems', 'edit');
const canServersView = hasPermission('servers', 'view');
const canServersEdit = hasPermission('servers', 'edit');
const canAdminsView = hasPermission('admins', 'view');
const canAdminsEdit = hasPermission('admins', 'edit');
const canUsersView = hasPermission('users', 'view');
const canUsersEdit = hasPermission('users', 'edit');
const canDistView = hasPermission('distribution', 'view');
document.getElementById('nav-app-users').style.display = currentUser.role === 'admin' || currentUser.role === 'moderator' ? '' : 'none';
document.getElementById('nav-audit').style.display = currentUser.role === 'admin' ? '' : 'none';
document.querySelector('[data-section="distribution"]').style.display = canDistView ? '' : 'none';
// Кнопки добавления
document.getElementById('btn-add-system').style.display = hasPermission('systems', 'edit') ? '' : 'none';
document.getElementById('btn-add-servers').style.display = hasPermission('servers', 'edit') ? '' : 'none';
document.getElementById('btn-import-servers').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('btn-add-admins').style.display = hasPermission('admins', 'edit') ? '' : 'none';
document.getElementById('btn-import-admins').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('btn-import-admins-1').style.display = currentUser.role === 'admin' ? '' : 'none';
document.getElementById('btn-add-users').style.display = hasPermission('users', 'edit') ? '' : 'none';
const canAddCommon = currentUser.role === 'admin';
document.getElementById('btn-add-common-todo').style.display = canAddCommon ? '' : 'none';
document.getElementById('btn-add-personal-todo').style.display = ''; // Все могут добавлять личные
}
document.getElementById('login-password').addEventListener('keypress', (e) => {
if (e.key === 'Enter') doLogin();
});
// Navigation
document.querySelectorAll('.nav button').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.section === 'logout') { doLogout(); return; }
document.querySelectorAll('.nav button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.section').forEach(s => s.classList.remove('on'));
document.getElementById(btn.dataset.section).classList.add('on');
if (btn.dataset.section === 'app-users') { renderAppUsers(); }
else if (btn.dataset.section === 'audit') { renderAudit(); }
else if (btn.dataset.section === 'profile') { loadProfile(); }
else { loadAll(); }
});
});
function toggleTheme() {
const isLight = document.documentElement.classList.toggle('light');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
document.getElementById('btn-theme').textContent = isLight ? '☀️' : '🌙';
}
async function loadAll() {
const headers = { 'X-User': currentUser ? currentUser.login : '' };
commonTodos = await fetch('api/common-todos', { headers }).then(r => r.json());
personalTodos = await fetch('api/personal-todos', { headers }).then(r => r.json());
systems = await fetch('api/systems', { headers }).then(r => r.json());
servers = await fetch('api/servers', { headers }).then(r => r.json());
admins = await fetch('api/admins', { headers }).then(r => r.json());
users = await fetch('api/users', { headers }).then(r => r.json());
distribution = await fetch('api/distribution', { headers }).then(r => r.json());
adminServers = await fetch('api/admin-servers', { headers }).then(r => r.json());
if (currentUser && currentUser.role === 'admin') {
appUsers = await fetch('api/app-users', { headers }).then(r => r.json());
auditLog = await fetch('api/audit', { headers }).then(r => r.json());
roles = await fetch('api/roles', { headers }).then(r => r.json());
}
renderDashboard();
renderCommonTodos();
renderPersonalTodos();
renderSystems();
renderServers();
renderAdmins();
renderUsers();
renderDist();
renderRoles();
}
function updatePermissions() {
if (!confirm('Обновить права всех модераторов на новую структуру?')) return;
fetch('api/update-permissions', { method: 'POST', headers: { 'X-User': currentUser.login } })
.then(r => r.json())
.then(data => {
if (data.ok) {
alert('Права обновлены! Перезайдите в систему.');
location.reload();
}
});
}
function renderDashboard() {
// Персональная статистика: общие + личные задачи
const userLogin = currentUser.login;
const myCommonTodos = (commonTodos || []).filter(t => {
const state = t.userStates && t.userStates[userLogin];
return !state || !state.completed;
});
const myPersonalTodos = (personalTodos || []).filter(t => !t.completed);
const allActive = myCommonTodos.length + myPersonalTodos.length;
const highPriority = [...myCommonTodos.filter(t => t.priority === 'high'), ...myPersonalTodos.filter(t => t.priority === 'high')].length;
document.getElementById('stat-todos').textContent = allActive;
document.getElementById('stat-todos-high').textContent = highPriority;
document.getElementById('stat-systems').textContent = systems.length;
document.getElementById('stat-servers').textContent = servers.length;
document.getElementById('stat-admins').textContent = admins.length;
document.getElementById('stat-users').textContent = users.length;
// Распределение по админам (из adminServers)
const byAdmin = {};
admins.forEach(a => {
const adminServersList = (adminServers || []).filter(s => s.adminId === a.id);
if (adminServersList.length > 0) {
byAdmin[a.name] = adminServersList.map(s => s.city + ' (' + s.cityCode + ') ' + s.ip);
}
});
const html = Object.entries(byAdmin).map(([admin, items]) =>
'<div style="margin: 12px 0; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; color: var(--text-primary);">' +
'<strong style="color: var(--text-primary);">' + admin + '</strong>: ' + items.join(', ') +
'</div>'
).join('') || '<p style="color: var(--text-secondary);">Нет данных</p>';
document.getElementById('dashboard-dist').innerHTML = html;
}
function swapTodoBlocks() {
todoBlocksSwapped = !todoBlocksSwapped;
const commonBlock = document.getElementById('common-todos-block');
const personalBlock = document.getElementById('personal-todos-block');
if (todoBlocksSwapped) {
personalBlock.style.order = '-1';
} else {
personalBlock.style.order = '0';
}
}
function renderCommonTodos() {
const search = document.getElementById('common-todo-search').value.toLowerCase();
const userLogin = currentUser.login;
const canEdit = currentUser.role === 'admin';
let filtered = (commonTodos || []).filter(t => (t.title || '').toLowerCase().includes(search));
document.getElementById('common-todos-body').innerHTML = filtered.map(t => {
const userState = (t.userStates && t.userStates[userLogin]) || { completed: false, completedAt: null };
const isCompleted = userState.completed === true;
return '<tr class="' + (isCompleted ? 'completed' : '') + '">' +
'<td>' + t.title + '</td>' +
'<td><span class="badge badge-' + (t.priority || 'low') + '">' + (t.priority === 'high' ? 'Высокий' : t.priority === 'medium' ? 'Средний' : 'Низкий') + '</span></td>' +
'<td>' + (t.deadline || '—') + '</td>' +
'<td>' + (t.createdBy || '—') + '</td>' +
'<td>' + (isCompleted ? '✓ Выполнено' + (userState.completedAt ? ' (' + userState.completedAt + ')' : '') : 'В работе') + '</td>' +
'<td class="actions">' +
'<button class="btn btn-sm btn-secondary" onclick="toggleCommonTodo(' + t.id + ')">' + (isCompleted ? 'Вернуть' : 'Готово') + '</button>' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editCommonTodo(' + t.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteCommonTodo(' + t.id + ')">✕</button>' : '') +
'</td></tr>';
}).join('') || '<tr><td colspan="6" style="text-align: center; color: #71717a;">Нет общих задач</td></tr>';
// Показываем кнопку добавления только админам
document.getElementById('btn-add-common-todo').style.display = canEdit ? '' : 'none';
}
function renderPersonalTodos() {
const search = document.getElementById('personal-todo-search').value.toLowerCase();
const filter = document.getElementById('personal-todo-filter').value;
let filtered = (personalTodos || []).filter(t => {
const matchSearch = (t.title || '').toLowerCase().includes(search);
const matchFilter = filter === '' || (filter === 'active' && !t.completed) || (filter === 'completed' && t.completed);
return matchSearch && matchFilter;
});
document.getElementById('personal-todos-body').innerHTML = filtered.map(t =>
'<tr class="' + (t.completed ? 'completed' : '') + '">' +
'<td>' + t.title + '</td>' +
'<td><span class="badge badge-' + (t.priority || 'low') + '">' + (t.priority === 'high' ? 'Высокий' : t.priority === 'medium' ? 'Средний' : 'Низкий') + '</span></td>' +
'<td>' + (t.deadline || '—') + '</td>' +
'<td>' + (t.completed ? '✓ Выполнено' : 'В работе') + '</td>' +
'<td class="actions">' +
'<button class="btn btn-sm btn-secondary" onclick="togglePersonalTodo(' + t.id + ')">' + (t.completed ? 'Вернуть' : 'Готово') + '</button>' +
'<button class="btn btn-sm btn-secondary" onclick="editPersonalTodo(' + t.id + ')">✏</button>' +
'<button class="btn btn-sm btn-danger" onclick="deletePersonalTodo(' + t.id + ')">✕</button>' +
'</td></tr>'
).join('') || '<tr><td colspan="5" style="text-align: center; color: #71717a;">Нет личных задач</td></tr>';
}
function renderSystems() {
const search = document.getElementById('system-search').value.toLowerCase();
const filtered = systems.filter(s =>
(s.name || '').toLowerCase().includes(search) ||
(s.address || '').toLowerCase().includes(search) ||
(s.description || '').toLowerCase().includes(search) ||
(s.responsible || '').toLowerCase().includes(search) ||
(s.support || '').toLowerCase().includes(search)
);
const canEdit = hasPermission('systems', 'edit');
document.getElementById('systems-body').innerHTML = filtered.map(s =>
'<tr><td>' + (s.name || '—') + '</td>' +
'<td>' + (s.address || '—') + '</td>' +
'<td>' + (s.description || '—') + '</td>' +
'<td>' + (s.responsible || '—') + '</td>' +
'<td>' + (s.support || '—') + '</td>' +
'<td class="actions">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editSystem(' + s.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteSystem(' + s.id + ')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">Нет систем</td></tr>';
}
function renderServers() {
const search = document.getElementById('server-search').value.toLowerCase();
let filtered = servers.filter(s =>
(s.city || '').toLowerCase().includes(search) ||
(s.region || '').toLowerCase().includes(search) ||
(s.cityCode || '').toLowerCase().includes(search) ||
(s.ip || '').toLowerCase().includes(search)
);
if (sortField) {
filtered.sort((a, b) => {
const va = (a[sortField] || '').toString().toLowerCase();
const vb = (b[sortField] || '').toString().toLowerCase();
return sortDesc ? vb.localeCompare(va) : va.localeCompare(vb);
});
}
const canEdit = hasPermission('servers', 'edit');
document.getElementById('servers-body').innerHTML = filtered.map(s =>
'<tr><td>' + (s.city || '—') + '</td>' +
'<td style="max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + (s.region || '—') + '</td>' +
'<td>' + (s.cityCode || '—') + '</td>' +
'<td>' + (s.ip || '—') + '</td>' +
'<td>' + (s.description || '—') + '</td>' +
'<td class="actions" style="width:100px;">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editServer(' + s.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteServer(' + s.id + ')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="6" style="text-align: center; color: #71717a;">Нет серверов</td></tr>';
}
document.querySelectorAll('#servers th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const field = th.dataset.sort;
if (sortField === field) sortDesc = !sortDesc;
else { sortField = field; sortDesc = false; }
renderServers();
});
});
document.querySelectorAll('#admins th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const field = th.dataset.sort;
if (sortFieldAdmin === field) sortDescAdmin = !sortDescAdmin;
else { sortFieldAdmin = field; sortDescAdmin = false; }
renderAdmins();
});
});
function renderAdmins() {
const search = document.getElementById('admin-search').value.toLowerCase();
const filtered = admins.filter(a => (a.name || '').toLowerCase().includes(search) || (a.position || '').toLowerCase().includes(search) || (a.email || '').toLowerCase().includes(search));
if (sortFieldAdmin) {
filtered.sort((a, b) => {
const va = (a[sortFieldAdmin] || '').toString().toLowerCase();
const vb = (b[sortFieldAdmin] || '').toString().toLowerCase();
return sortDescAdmin ? vb.localeCompare(va) : va.localeCompare(vb);
});
}
const canEdit = hasPermission('admins', 'edit');
const rows = [];
filtered.forEach(a => {
const servers = (adminServers || []).filter(s => s.adminId === a.id);
if (servers.length === 0) {
rows.push('<tr><td>' + (a.name || '—') + '</td>' +
'<td>' + (a.position || '—') + '</td>' +
'<td>' + (a.workPhone || '—') + '</td>' +
'<td>' + (a.homePhone || '—') + '</td>' +
'<td>' + (a.mobilePhone || '—') + '</td>' +
'<td>' + (a.email || '—') + '</td>' +
'<td><span style="color:var(--text-secondary);">Нет серверов</span></td>' +
'<td class="actions">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editAdmin(' + a.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteAdmin(' + a.id + ')">✕</button>' : '') +
'</td></tr>');
} else {
servers.forEach((s, idx) => {
rows.push('<tr>' +
(idx === 0 ? '<td rowspan="' + servers.length + '">' + (a.name || '—') + '</td>' +
'<td rowspan="' + servers.length + '">' + (a.position || '—') + '</td>' +
'<td rowspan="' + servers.length + '">' + (a.workPhone || '—') + '</td>' +
'<td rowspan="' + servers.length + '">' + (a.homePhone || '—') + '</td>' +
'<td rowspan="' + servers.length + '">' + (a.mobilePhone || '—') + '</td>' +
'<td rowspan="' + servers.length + '">' + (a.email || '—') + '</td>' : '') +
'<td>' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</td>' +
'<td class="actions">' +
(idx === 0 ? ((canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editAdmin(' + a.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteAdmin(' + a.id + ')">✕</button>' : '')) : '') +
'</td></tr>');
});
}
});
document.getElementById('admins-body').innerHTML = rows.join('') || '<tr><td colspan="7" style="text-align: center; color: var(--text-secondary);">Нет админов</td></tr>';
}
function refreshAdmins() { renderAdmins(); }
function renderUsers() {
const search = document.getElementById('user-search').value.toLowerCase();
const filtered = users.filter(u => (u.name || '').toLowerCase().includes(search) || (u.login || '').toLowerCase().includes(search));
const canEdit = hasPermission('users', 'edit');
document.getElementById('users-body').innerHTML = filtered.map(u =>
'<tr><td>' + (u.name || '—') + '</td>' +
'<td>' + (u.login || '—') + '</td>' +
'<td>' + (u.system || '—') + '</td>' +
'<td>' + (u.position || '—') + '</td>' +
'<td>' + (u.city || '—') + '</td>' +
'<td>' + (u.userIp || '—') + '</td>' +
'<td class="actions">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editUser(' + u.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteUser(' + u.id + ')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="7" style="text-align: center; color: var(--text-secondary);">Нет пользователей</td></tr>';
}
function renderUsers() {
const search = document.getElementById('user-search').value.toLowerCase();
const filtered = users.filter(u => (u.name || '').toLowerCase().includes(search) || (u.login || '').toLowerCase().includes(search));
const canEdit = hasPermission('users', 'edit');
document.getElementById('users-body').innerHTML = filtered.map(u =>
'<tr><td>' + (u.name || '—') + '</td>' +
'<td>' + (u.login || '—') + '</td>' +
'<td>' + (u.system || '—') + '</td>' +
'<td>' + (u.basis || '—') + '</td>' +
'<td>' + (u.mode || '—') + '</td>' +
'<td>' + (u.extra || '—') + '</td>' +
'<td>' + (u.date || '—') + '</td>' +
'<td class="actions">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editUser(' + u.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteUser(' + u.id + ')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="8" style="text-align: center; color: #71717a;">Нет пользователей</td></tr>';
}
function sortDist(field) {
if (distSort.field === field) {
distSort.dir = -distSort.dir;
} else {
distSort.field = field;
distSort.dir = 1;
}
document.querySelectorAll('#distribution thead th span').forEach(s => s.textContent = '↕');
document.getElementById('sort-' + field).textContent = distSort.dir === 1 ? '↑' : '↓';
renderDist();
}
function renderDist() {
const search = document.getElementById('dist-search').value.toLowerCase();
// Берём данные из adminServers + admins (вкладка Коллеги-админы)
let distData = (adminServers || []).map(s => {
const admin = admins.find(a => a.id === s.adminId);
return { id: s.id, admin: admin ? admin.name : '—', city: s.city, cityCode: s.cityCode, ip: s.ip, adminId: s.adminId };
});
// Сортировка
distData.sort((a, b) => {
const aVal = (a[distSort.field] || '').toString().toLowerCase();
const bVal = (b[distSort.field] || '').toString().toLowerCase();
if (aVal < bVal) return -1 * distSort.dir;
if (aVal > bVal) return 1 * distSort.dir;
return 0;
});
const canEdit = hasPermission('distribution', 'edit');
const filtered = distData.filter(d => (d.admin || '').toLowerCase().includes(search) || (d.city || '').toLowerCase().includes(search));
document.getElementById('dist-body').innerHTML = filtered.map(d =>
'<tr><td>' + (d.admin || '—') + '</td>' +
'<td>' + (d.city || '—') + '</td>' +
'<td>' + (d.cityCode || '—') + '</td>' +
'<td>' + (d.ip || '—') + '</td>' +
'<td class="actions">' +
(canEdit ? '<button class="btn btn-sm btn-secondary" onclick="editAdminServer(' + d.id + ')">✏</button>' : '') +
(canEdit ? '<button class="btn btn-sm btn-danger" onclick="deleteAdminServer(' + d.id + ')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="5" style="text-align: center; color: var(--text-secondary);">Нет записей</td></tr>';
}
// Modal helpers
function openModal(title, html, onSave, onOpen) {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').innerHTML = html;
document.getElementById('modal-save').onclick = onSave;
document.getElementById('modal').classList.add('on');
if (onOpen) onOpen();
}
function closeModal() { document.getElementById('modal').classList.remove('on'); editId = null; }
// TODOS
// COMMON TODOS
function openCommonTodoModal() {
if (currentUser.role !== 'admin') { alert('Нет прав'); return; }
editId = null;
openModal('Добавить общую задачу',
'<div class="form-row"><div><label>Задача</label><input id="m-common-title"></div></div>' +
'<div class="form-row"><div><label>Приоритет</label><select id="m-common-priority"><option value="high">Высокий</option><option value="medium" selected>Средний</option><option value="low">Низкий</option></select></div>' +
'<div><label>Дедлайн</label><input type="date" id="m-common-deadline"></div></div>',
() => {
const item = { title: document.getElementById('m-common-title').value, priority: document.getElementById('m-common-priority').value, deadline: document.getElementById('m-common-deadline').value };
fetch('api/common-todos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify(item) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editCommonTodo(id) {
if (currentUser.role !== 'admin') { alert('Нет прав'); return; }
editId = id;
const t = commonTodos.find(x => x.id === id);
openModal('Редактировать общую задачу',
'<div class="form-row"><div><label>Задача</label><input id="m-common-title" value="' + (t.title || '') + '"></div></div>' +
'<div class="form-row"><div><label>Приоритет</label><select id="m-common-priority"><option value="high"' + (t.priority === 'high' ? ' selected' : '') + '>Высокий</option><option value="medium"' + (t.priority === 'medium' ? ' selected' : '') + '>Средний</option><option value="low"' + (t.priority === 'low' ? ' selected' : '') + '>Низкий</option></select></div>' +
'<div><label>Дедлайн</label><input type="date" id="m-common-deadline" value="' + (t.deadline || '') + '"></div></div>',
() => {
fetch('api/common-todos/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: document.getElementById('m-common-title').value, priority: document.getElementById('m-common-priority').value, deadline: document.getElementById('m-common-deadline').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function toggleCommonTodo(id) {
const t = commonTodos.find(x => x.id === id);
const userLogin = currentUser.login;
const currentState = (t.userStates && t.userStates[userLogin]) || { completed: false, completedAt: null };
const newState = { completed: !currentState.completed, completedAt: !currentState.completed ? new Date().toISOString().slice(0,10) : null };
fetch('api/common-todos/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': userLogin }, body: JSON.stringify({ userState: newState }) }).then(() => loadAll());
}
function deleteCommonTodo(id) { if (confirm('Удалить общую задачу?')) { fetch('api/common-todos/' + id, { method: 'DELETE', headers: { 'X-User': currentUser.login } }).then(() => loadAll()); } }
// PERSONAL TODOS
function openPersonalTodoModal() {
editId = null;
openModal('Добавить личную задачу',
'<div class="form-row"><div><label>Задача</label><input id="m-personal-title"></div></div>' +
'<div class="form-row"><div><label>Приоритет</label><select id="m-personal-priority"><option value="high">Высокий</option><option value="medium" selected>Средний</option><option value="low">Низкий</option></select></div>' +
'<div><label>Дедлайн</label><input type="date" id="m-personal-deadline"></div></div>',
() => {
const item = { title: document.getElementById('m-personal-title').value, priority: document.getElementById('m-personal-priority').value, deadline: document.getElementById('m-personal-deadline').value };
fetch('api/personal-todos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify(item) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editPersonalTodo(id) {
editId = id;
const t = personalTodos.find(x => x.id === id);
openModal('Редактировать личную задачу',
'<div class="form-row"><div><label>Задача</label><input id="m-personal-title" value="' + (t.title || '') + '"></div></div>' +
'<div class="form-row"><div><label>Приоритет</label><select id="m-personal-priority"><option value="high"' + (t.priority === 'high' ? ' selected' : '') + '>Высокий</option><option value="medium"' + (t.priority === 'medium' ? ' selected' : '') + '>Средний</option><option value="low"' + (t.priority === 'low' ? ' selected' : '') + '>Низкий</option></select></div>' +
'<div><label>Дедлайн</label><input type="date" id="m-personal-deadline" value="' + (t.deadline || '') + '"></div></div>',
() => {
fetch('api/personal-todos/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: document.getElementById('m-personal-title').value, priority: document.getElementById('m-personal-priority').value, deadline: document.getElementById('m-personal-deadline').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function togglePersonalTodo(id) {
const t = personalTodos.find(x => x.id === id);
fetch('api/personal-todos/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ completed: !t.completed, completedAt: !t.completed ? new Date().toISOString().slice(0,10) : null }) }).then(() => loadAll());
}
function deletePersonalTodo(id) { if (confirm('Удалить личную задачу?')) { fetch('api/personal-todos/' + id, { method: 'DELETE', headers: { 'X-User': currentUser.login } }).then(() => loadAll()); } }
// SYSTEMS
function openSystemModal() {
if (!hasPermission('systems', 'edit')) { alert('Нет прав'); return; }
editId = null;
openModal('Добавить систему',
'<div class="form-row"><div><label>Система</label><input id="m-name"></div></div>' +
'<div class="form-row"><div><label>Адрес</label><input id="m-address"></div>' +
'<div><label>Описание</label><input id="m-description"></div></div>' +
'<div class="form-row"><div><label>Ответственный</label><input id="m-responsible"></div>' +
'<div><label>Техподдержка</label><input id="m-support"></div></div>',
() => {
fetch('api/systems', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('m-name').value, address: document.getElementById('m-address').value, description: document.getElementById('m-description').value, responsible: document.getElementById('m-responsible').value, support: document.getElementById('m-support').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editSystem(id) {
editId = id;
const s = systems.find(x => x.id === id);
openModal('Редактировать систему',
'<div class="form-row"><div><label>Система</label><input id="m-name" value="' + (s.name || '') + '"></div></div>' +
'<div class="form-row"><div><label>Адрес</label><input id="m-address" value="' + (s.address || '') + '"></div>' +
'<div><label>Описание</label><input id="m-description" value="' + (s.description || '') + '"></div></div>' +
'<div class="form-row"><div><label>Ответственный</label><input id="m-responsible" value="' + (s.responsible || '') + '"></div>' +
'<div><label>Техподдержка</label><input id="m-support" value="' + (s.support || '') + '"></div></div>',
() => {
fetch('api/systems/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('m-name').value, address: document.getElementById('m-address').value, description: document.getElementById('m-description').value, responsible: document.getElementById('m-responsible').value, support: document.getElementById('m-support').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function deleteSystem(id) { if (confirm('Удалить систему?')) { fetch('api/systems/' + id, { method: 'DELETE' }).then(() => loadAll()); } }
// SERVERS
async function importServers() {
if (!confirm('Импортировать серверы из файл "список серверов.txt"?')) return;
try {
const res = await fetch('api/import-servers', { method: 'POST', headers: { 'X-User': currentUser.login } });
const result = await res.json();
if (result.ok) {
alert('Импортировано записей: ' + result.count);
loadAll();
} else {
alert('Ошибка: ' + result.error);
}
} catch (e) {
alert('Ошибка импорта: ' + e.message);
}
}
async function importAdmins() {
if (!confirm('Импортировать админов из файл "список админов.txt"?')) return;
try {
const res = await fetch('api/import-admins', { method: 'POST', headers: { 'X-User': currentUser.login } });
const result = await res.json();
if (result.ok) {
alert('Добавлено админов: ' + result.count);
loadAll();
} else {
alert('Ошибка: ' + result.error);
}
} catch (e) {
alert('Ошибка импорта: ' + e.message);
}
}
async function importAdmins1() {
if (!confirm('Импортировать и обновить админов из файл "список админов-1.txt"? Существующие записи будут обновлены.')) return;
try {
const res = await fetch('api/import-admins-1', { method: 'POST', headers: { 'X-User': currentUser.login } });
const result = await res.json();
if (result.ok) {
alert('Обновлено: ' + result.updated + ', добавлено: ' + result.added);
loadAll();
} else {
alert('Ошибка: ' + result.error);
}
} catch (e) {
alert('Ошибка импорта: ' + e.message);
}
}
function openServerModal() {
if (!hasPermission('servers', 'edit')) { alert('Нет прав'); return; }
editId = null;
openModal('Добавить сервер',
'<div class="form-row"><div><label>Город</label><input id="m-city"></div>' +
'<div><label>Код города</label><input id="m-citycode"></div></div>' +
'<div class="form-row"><div><label>Регион</label><input id="m-region" style="max-width:200px;"></div>' +
'<div><label>IP</label><input id="m-ip"></div></div>' +
'<div><label>Описание</label><textarea id="m-description" rows="3" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);"></textarea></div>',
() => {
fetch('api/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ city: document.getElementById('m-city').value, region: document.getElementById('m-region').value, cityCode: document.getElementById('m-citycode').value, ip: document.getElementById('m-ip').value, description: document.getElementById('m-description').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editServer(id) {
editId = id;
const s = servers.find(x => x.id === id);
openModal('Редактировать сервер',
'<div class="form-row"><div><label>Город</label><input id="m-city" value="' + (s.city || '') + '"></div>' +
'<div><label>Код города</label><input id="m-citycode" value="' + (s.cityCode || '') + '"></div></div>' +
'<div class="form-row"><div><label>Регион</label><input id="m-region" value="' + (s.region || '') + '" style="max-width:200px;"></div>' +
'<div><label>IP</label><input id="m-ip" value="' + (s.ip || '') + '"></div></div>' +
'<div><label>Описание</label><textarea id="m-description" rows="3" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);">' + (s.description || '') + '</textarea></div>',
() => {
fetch('api/servers/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ city: document.getElementById('m-city').value, region: document.getElementById('m-region').value, cityCode: document.getElementById('m-citycode').value, ip: document.getElementById('m-ip').value, description: document.getElementById('m-description').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function deleteServer(id) { if (confirm('Удалить сервер?')) { fetch('api/servers/' + id, { method: 'DELETE' }).then(() => loadAll()); } }
// ADMINS
function openAdminModal() {
if (!hasPermission('admins', 'edit')) { alert('Нет прав'); return; }
editId = null;
openModal('Добавить админа',
'<div class="form-row"><div><label>ФИО</label><input id="m-name"></div>' +
'<div><label>Должность</label><input id="m-position"></div></div>' +
'<div class="form-row"><div><label>Рабочий тел</label><input id="m-workphone"></div>' +
'<div><label>Домашний тел</label><input id="m-homephone"></div></div>' +
'<div class="form-row"><div><label>Сотовый</label><input id="m-mobilephone"></div>' +
'<div><label>Email</label><input id="m-email"></div></div>',
() => {
fetch('api/admins', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ name: document.getElementById('m-name').value, position: document.getElementById('m-position').value, workPhone: document.getElementById('m-workphone').value, homePhone: document.getElementById('m-homephone').value, mobilePhone: document.getElementById('m-mobilephone').value, email: document.getElementById('m-email').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editAdmin(id) {
editId = id;
currentAdminId = id;
const a = admins.find(x => x.id === id);
const adminServersList = (adminServers || []).filter(s => s.adminId === a.id);
const sortedServers = [...servers].sort((a, b) => (a.city || '').localeCompare(b.city || ''));
const serverOptions = '<option value="">-- Выберите сервер --</option>' + sortedServers.map(s =>
'<option value="' + s.id + '">' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</option>'
).join('');
const assignedServerIds = new Set(adminServersList.map(s => s.serverId));
const availableServers = sortedServers.filter(s => !assignedServerIds.has(s.id));
const availableOptions = '<option value="">-- Выберите сервер --</option>' + availableServers.map(s =>
'<option value="' + s.id + '">' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</option>'
).join('');
const serversHtml = adminServersList.length > 0
? '<div style="margin-top:16px;max-height:200px;overflow-y:auto;">' +
adminServersList.map(s =>
'<div style="display:flex;justify-content:space-between;align-items:center;padding:8px;background:var(--bg-tertiary);border-radius:6px;margin-bottom:8px;">' +
'<span>' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</span>' +
'<div style="display:flex;gap:8px;">' +
'<button class="btn btn-sm btn-secondary" onclick="editAdminServer(' + s.id + ')">✏</button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteAdminServerFromModal(' + s.id + ')">✕</button>' +
'</div></div>'
).join('') +
'</div>'
: '<p style="color:var(--text-secondary);margin-top:16px;">Серверы не назначены</p>';
openModal('Редактировать админа',
'<div style="background:var(--bg-tertiary);padding:16px;border-radius:8px;margin-bottom:16px;">' +
'<h3 style="margin-bottom:12px;">Информация об админе</h3>' +
'<div class="form-row"><div><label>ФИО</label><input id="m-name" value="' + (a.name || '') + '"></div>' +
'<div><label>Должность</label><input id="m-position" value="' + (a.position || '') + '"></div></div>' +
'<div class="form-row"><div><label>Рабочий тел</label><input id="m-workphone" value="' + (a.workPhone || '') + '"></div>' +
'<div><label>Домашний тел</label><input id="m-homephone" value="' + (a.homePhone || '') + '"></div></div>' +
'<div class="form-row"><div><label>Сотовый</label><input id="m-mobilephone" value="' + (a.mobilePhone || '') + '"></div>' +
'<div><label>Email</label><input id="m-email" value="' + (a.email || '') + '"></div></div>' +
'</div>' +
'<div style="background:var(--bg-tertiary);padding:16px;border-radius:8px;">' +
'<h3 style="margin-bottom:12px;">Серверы</h3>' +
'<div class="form-row"><div><label>Добавить сервер:</label><select id="m-server-select" style="width:100%;">' + availableOptions + '</select></div></div>' +
'<button class="btn btn-sm btn-secondary" onclick="addServerToAdminFromModal(' + a.id + ')" style="margin-top:8px;">+ Добавить сервер</button>' +
serversHtml +
'</div>',
() => {
// Сохранение информации об админе
fetch('api/admins/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ name: document.getElementById('m-name').value, position: document.getElementById('m-position').value, workPhone: document.getElementById('m-workphone').value, homePhone: document.getElementById('m-homephone').value, mobilePhone: document.getElementById('m-mobilephone').value, email: document.getElementById('m-email').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function deleteAdmin(id) { if (confirm('Удалить админа?')) { fetch('api/admins/' + id, { method: 'DELETE', headers: { 'X-User': currentUser.login } }).then(() => loadAll()); } }
function refreshAdminModal() {
if (!currentAdminId) return;
const a = admins.find(x => x.id === currentAdminId);
if (!a) return;
const adminServersList = (adminServers || []).filter(s => s.adminId === a.id);
const sortedServers = [...servers].sort((a, b) => (a.city || '').localeCompare(b.city || ''));
const assignedServerIds = new Set(adminServersList.map(s => s.serverId));
const availableServers = sortedServers.filter(s => !assignedServerIds.has(s.id));
const availableOptions = '<option value="">-- Выберите сервер --</option>' + availableServers.map(s =>
'<option value="' + s.id + '">' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</option>'
).join('');
const serversHtml = adminServersList.length > 0
? '<div style="margin-top:16px;max-height:200px;overflow-y:auto;">' +
adminServersList.map(s =>
'<div style="display:flex;justify-content:space-between;align-items:center;padding:8px;background:var(--bg-tertiary);border-radius:6px;margin-bottom:8px;">' +
'<span>' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</span>' +
'<div style="display:flex;gap:8px;">' +
'<button class="btn btn-sm btn-secondary" onclick="editAdminServer(' + s.id + ')">✏</button>' +
'<button class="btn btn-sm btn-danger" onclick="deleteAdminServerFromModal(' + s.id + ')">✕</button>' +
'</div></div>'
).join('') +
'</div>'
: '<p style="color:var(--text-secondary);margin-top:16px;">Серверы не назначены</p>';
document.getElementById('modal-body').innerHTML =
'<div style="background:var(--bg-tertiary);padding:16px;border-radius:8px;margin-bottom:16px;">' +
'<h3 style="margin-bottom:12px;">Информация об админе</h3>' +
'<div class="form-row"><div><label>ФИО</label><input id="m-name" value="' + (a.name || '') + '"></div>' +
'<div><label>Должность</label><input id="m-position" value="' + (a.position || '') + '"></div></div>' +
'<div class="form-row"><div><label>Рабочий тел</label><input id="m-workphone" value="' + (a.workPhone || '') + '"></div>' +
'<div><label>Домашний тел</label><input id="m-homephone" value="' + (a.homePhone || '') + '"></div></div>' +
'<div class="form-row"><div><label>Сотовый</label><input id="m-mobilephone" value="' + (a.mobilePhone || '') + '"></div>' +
'<div><label>Email</label><input id="m-email" value="' + (a.email || '') + '"></div></div>' +
'</div>' +
'<div style="background:var(--bg-tertiary);padding:16px;border-radius:8px;">' +
'<h3 style="margin-bottom:12px;">Серверы</h3>' +
'<div class="form-row"><div><label>Добавить сервер:</label><select id="m-server-select" style="width:100%;">' + availableOptions + '</select></div></div>' +
'<button class="btn btn-sm btn-secondary" onclick="addServerToAdminFromModal(' + a.id + ')" style="margin-top:8px;">+ Добавить сервер</button>' +
serversHtml +
'</div>';
}
function addAdminServer(adminId, adminName) {
if (!hasPermission('admins', 'edit')) { alert('Нет прав'); return; }
editId = null;
const sortedServers = [...servers].sort((a, b) => (a.city || '').localeCompare(b.city || ''));
const serverOptions = sortedServers.length > 0
? sortedServers.map(s => '<option value="' + s.id + '">' + (s.city || '—') + ' (' + (s.cityCode || '—') + ') ' + (s.ip || '—') + '</option>').join('')
: '<option value="">Нет серверов в списке</option>';
openModal('Добавить сервер: ' + adminName,
'<div><label>Выберите сервер из списка:</label><select id="m-server-select" style="width:100%;padding:10px;border-radius:6px;border:1px solid #3f3f46;background:#27272a;color:#f4f4f5;">' + serverOptions + '</select></div>',
() => {
const serverId = document.getElementById('m-server-select').value;
const server = servers.find(s => s.id == serverId);
if (server) {
fetch('api/admin-servers', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ adminId, serverId: server.id, city: server.city, cityCode: server.cityCode, ip: server.ip }) }).then(() => { closeModal(); loadAll(); renderAdmins(); });
}
}
);
}
function editAdminServer(id) {
const s = adminServers.find(x => x.id === id);
if (!s) return;
const sortedServers = [...servers].sort((a, b) => (a.city || '').localeCompare(b.city || ''));
const serverOptions = sortedServers.length > 0
? sortedServers.map(serv => '<option value="' + serv.id + '"' + (serv.id === s.serverId ? ' selected' : '') + '>' + (serv.city || '—') + ' (' + (serv.cityCode || '—') + ') ' + (serv.ip || '—') + '</option>').join('')
: '<option value="">Нет серверов в списке</option>';
openModal('Редактировать сервер',
'<div><label>Выберите сервер из списка:</label><select id="m-server-select" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);">' + serverOptions + '</select></div>',
() => {
const serverId = document.getElementById('m-server-select').value;
const server = servers.find(s => s.id == serverId);
if (server) {
fetch('api/admin-servers/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ serverId: server.id, city: server.city, cityCode: server.cityCode, ip: server.ip }) })
.then(() => {
closeModal();
loadAll();
setTimeout(() => editAdmin(currentAdminId), 100); // Вернуться к редактированию админа
});
}
}
);
}
function deleteAdminServer(id) { if (confirm('Удалить сервер?')) { fetch('api/admin-servers/' + id, { method: 'DELETE', headers: { 'X-User': currentUser.login } }).then(() => { loadAll(); renderAdmins(); }); } }
function addServerToAdminFromModal(adminId) {
const serverId = document.getElementById('m-server-select').value;
if (!serverId) { alert('Выберите сервер'); return; }
const server = servers.find(s => s.id == serverId);
if (server) {
fetch('api/admin-servers', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ adminId, serverId: server.id, city: server.city, cityCode: server.cityCode, ip: server.ip }) })
.then(() => loadAll())
.then(() => {
refreshAdminModal();
renderAdmins();
});
}
}
function deleteAdminServerFromModal(id) {
if (confirm('Удалить сервер?')) {
fetch('api/admin-servers/' + id, { method: 'DELETE', headers: { 'X-User': currentUser.login } })
.then(() => loadAll())
.then(() => {
refreshAdminModal();
renderAdmins();
});
}
}
// USERS
function openUserModal() {
if (!hasPermission('users', 'edit')) { alert('Нет прав'); return; }
editId = null;
openModal('Добавить пользователя',
'<div class="form-row"><div><label>ФИО</label><input id="m-name"></div>' +
'<div><label>Логин</label><input id="m-login"></div></div>' +
'<div class="form-row"><div><label>Система</label><input id="m-system"></div>' +
'<div><label>Основание</label><input id="m-basis"></div></div>' +
'<div class="form-row"><div><label>Режим</label><input id="m-mode"></div>' +
'<div><label>Доп.</label><input id="m-extra"></div>' +
'<div><label>Дата</label><input type="date" id="m-date"></div></div>',
() => {
fetch('api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('m-name').value, login: document.getElementById('m-login').value, system: document.getElementById('m-system').value, basis: document.getElementById('m-basis').value, mode: document.getElementById('m-mode').value, extra: document.getElementById('m-extra').value, date: document.getElementById('m-date').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function editUser(id) {
editId = id;
const u = users.find(x => x.id === id);
openModal('Редактировать пользователя',
'<div class="form-row"><div><label>ФИО</label><input id="m-name" value="' + (u.name || '') + '"></div>' +
'<div><label>Логин</label><input id="m-login" value="' + (u.login || '') + '"></div></div>' +
'<div class="form-row"><div><label>Система</label><input id="m-system" value="' + (u.system || '') + '"></div>' +
'<div><label>Основание</label><input id="m-basis" value="' + (u.basis || '') + '"></div></div>' +
'<div class="form-row"><div><label>Режим</label><input id="m-mode" value="' + (u.mode || '') + '"></div>' +
'<div><label>Доп.</label><input id="m-extra" value="' + (u.extra || '') + '"></div>' +
'<div><label>Дата</label><input type="date" id="m-date" value="' + (u.date || '') + '"></div></div>',
() => {
fetch('api/users/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: document.getElementById('m-name').value, login: document.getElementById('m-login').value, system: document.getElementById('m-system').value, basis: document.getElementById('m-basis').value, mode: document.getElementById('m-mode').value, extra: document.getElementById('m-extra').value, date: document.getElementById('m-date').value }) }).then(() => { closeModal(); loadAll(); });
}
);
}
function deleteUser(id) { if (confirm('Удалить пользователя?')) { fetch('api/users/' + id, { method: 'DELETE' }).then(() => loadAll()); } }
// DISTRIBUTION
function openDistModal() {
if (!hasPermission('distribution', 'edit')) { alert('Нет прав'); return; }
editId = null;
const adminOptions = admins.length > 0
? admins.map(a => '<option value="' + (a.name || '') + '" data-city="' + (a.city || '') + '" data-code="' + (a.cityCode || '') + '">' + (a.name || 'Без имени') + '</option>').join('')
: '<option value="">Нет админов в списке</option>';
openModal('Добавить запись',
'<div class="form-row"><div><label>Админ (из списка)</label><select id="m-admin-select" onchange="document.getElementById(\'m-city\').value=this.options[this.selectedIndex].getAttribute(\'data-city\');document.getElementById(\'m-citycode\').value=this.options[this.selectedIndex].getAttribute(\'data-code\')">' + adminOptions + '</select></div></div>' +
'<div class="form-row"><div><label>Город (авто)</label><input id="m-city" readonly style="background:#27272a"></div>' +
'<div><label>Код города (авто)</label><input id="m-citycode" readonly style="background:#27272a"></div></div>' +
'<div><label>IP сервера</label><input id="m-ip" placeholder="10.10.10.1"></div>',
() => {
fetch('api/distribution', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ admin: document.getElementById('m-admin-select').value, city: document.getElementById('m-city').value, cityCode: document.getElementById('m-citycode').value, ip: document.getElementById('m-ip').value }) }).then(() => { closeModal(); loadAll(); renderDist(); });
}
);
}
function editDist(id) {
editId = id;
const d = distribution.find(x => x.id === id);
const adminOptions = admins.length > 0
? admins.map(a => '<option value="' + (a.name || '') + '"' + (a.name === d.admin ? ' selected' : '') + ' data-city="' + (a.city || '') + '" data-code="' + (a.cityCode || '') + '">' + (a.name || 'Без имени') + '</option>').join('')
: '<option value="">Нет админов в списке</option>';
openModal('Редактировать запись',
'<div class="form-row"><div><label>Админ (из списка)</label><select id="m-admin-select" onchange="document.getElementById(\'m-city\').value=this.options[this.selectedIndex].getAttribute(\'data-city\');document.getElementById(\'m-citycode\').value=this.options[this.selectedIndex].getAttribute(\'data-code\')">' + adminOptions + '</select></div></div>' +
'<div class="form-row"><div><label>Город (авто)</label><input id="m-city" value="' + (d.city || '') + '" readonly style="background:#27272a"></div>' +
'<div><label>Код города (авто)</label><input id="m-citycode" value="' + (d.cityCode || '') + '" readonly style="background:#27272a"></div></div>' +
'<div><label>IP сервера</label><input id="m-ip" value="' + (d.ip || '') + '"></div>',
() => {
fetch('api/distribution/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ admin: document.getElementById('m-admin-select').value, city: document.getElementById('m-city').value, cityCode: document.getElementById('m-citycode').value, ip: document.getElementById('m-ip').value }) }).then(() => { closeModal(); loadAll(); renderDist(); });
}
);
}
function deleteDist(id) { if (confirm('Удалить запись?')) { fetch('api/distribution/' + id, { method: 'DELETE' }).then(() => loadAll()); } }
// Reports
function reportTodos(days) {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
const userLogin = currentUser.login;
// Общие выполненные
const completedCommon = (commonTodos || []).filter(t => {
const state = t.userStates && t.userStates[userLogin];
return state && state.completed && state.completedAt && new Date(state.completedAt) >= cutoff;
});
// Личные выполненные
const completedPersonal = (personalTodos || []).filter(t => t.completed && t.completedAt && new Date(t.completedAt) >= cutoff);
const all = [...completedCommon, ...completedPersonal];
alert('Выполнено за период: ' + all.length + '\n\n' + all.map(t => '✓ ' + t.title + ' (' + (t.completedAt || (t.userStates[userLogin] || {}).completedAt) + ')').join('\n') || 'Нет выполненных');
}
function reportTodosCustom() {
const from = document.getElementById('report-from').value;
const to = document.getElementById('report-to').value;
if (!from || !to) { alert('Выберите даты'); return; }
const userLogin = currentUser.login;
const completedCommon = (commonTodos || []).filter(t => {
const state = t.userStates && t.userStates[userLogin];
return state && state.completed && state.completedAt && state.completedAt >= from && state.completedAt <= to;
});
const completedPersonal = (personalTodos || []).filter(t => t.completed && t.completedAt && t.completedAt >= from && t.completedAt <= to);
const all = [...completedCommon, ...completedPersonal];
alert('Выполнено за период: ' + all.length + '\n\n' + all.map(t => '✓ ' + t.title + ' (' + (t.completedAt || (t.userStates[userLogin] || {}).completedAt) + ')').join('\n') || 'Нет выполненных');
}
function exportTodosExcel() {
const userLogin = currentUser.login;
const rows = [['Задача', 'Тип', 'Приоритет', 'Дедлайн', 'Статус у меня', 'Дата выполнения', 'Создал']];
(commonTodos || []).forEach(t => {
const state = t.userStates && t.userStates[userLogin] || { completed: false, completedAt: null };
rows.push([t.title, 'Общая', t.priority || '', t.deadline || '', state.completed ? 'Выполнено' : 'В работе', state.completedAt || '', t.createdBy || '']);
});
(personalTodos || []).forEach(t => {
rows.push([t.title, 'Личная', t.priority || '', t.deadline || '', t.completed ? 'Выполнено' : 'В работе', t.completedAt || '', t.createdBy || '']);
});
const csv = '\uFEFF' + rows.map(r => r.map(v => '"' + String(v || '').replace(/"/g, '""') + '"').join(';')).join('\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'zadachi_otchet.csv';
a.click();
}
// APP USERS - SYNC
function syncAdminsToUsers() {
if (currentUser.role !== 'admin') { alert('Только администратор может синхронизировать'); return; }
if (!confirm('Создать пользователей приложения для всех админов из вкладки Коллеги-админы? Существующие пользователи будут обновлены.')) return;
fetch('api/sync-admins-to-users', { method: 'POST', headers: { 'X-User': currentUser.login } })
.then(r => r.json())
.then(result => {
alert('Готово!\nСоздано: ' + result.created + '\nОбновлено: ' + result.updated);
loadAll();
})
.catch(err => alert('Ошибка: ' + err));
}
// APP USERS
function renderAppUsers() {
const search = document.getElementById('appuser-search').value.toLowerCase();
const filtered = appUsers.filter(u => (u.login || '').toLowerCase().includes(search) || (u.name || '').toLowerCase().includes(search));
const canEditUsers = currentUser.role === 'admin';
document.getElementById('app-users-body').innerHTML = filtered.map(u => {
const phones = [u.workPhone, u.mobilePhone].filter(p => p && p.trim() !== '').join(' / ') || '—';
return '<tr><td>' + (u.login || '—') + '</td>' +
'<td>' + (u.name || '—') + '</td>' +
'<td>' + (u.position || '—') + '</td>' +
'<td>' + phones + '</td>' +
'<td>' + (u.email || '—') + '</td>' +
'<td>' + (u.city || '—') + '</td>' +
'<td>' + (u.cityCode || '—') + '</td>' +
'<td>' + ((u.ipAddress || u.dbAddress) || '—') + '</td>' +
'<td><span class="badge badge-' + (u.role === 'admin' ? 'high' : u.role === 'moderator' ? 'medium' : 'low') + '">' + (u.role === 'admin' ? 'Админ' : u.role === 'moderator' ? 'Модератор' : 'Оператор') + '</span></td>' +
'<td class="actions">' +
(canEditUsers ? '<button class="btn btn-sm btn-primary" onclick="editAppUserPermissions(\'' + u.login + '\')">🔐 Права</button>' : '') +
(canEditUsers ? '<button class="btn btn-sm btn-secondary" onclick="editAppUser(\'' + u.login + '\')">✏</button>' : '') +
(canEditUsers ? '<button class="btn btn-sm btn-danger" onclick="deleteAppUser(\'' + u.login + '\')">✕</button>' : '') +
'</td></tr>';
}).join('') || '<tr><td colspan="10" style="text-align: center; color: var(--text-secondary);">Нет пользователей</td></tr>';
}
function openAppUserModal() {
editId = null;
openModal('Добавить пользователя',
'<div class="form-row"><div><label>Логин</label><input id="m-login"></div>' +
'<div><label>Пароль</label><input type="password" id="m-password"></div></div>' +
'<div class="form-row"><div><label>ФИО</label><input id="m-name"></div>' +
'<div><label>Должность</label><input id="m-position"></div></div>' +
'<div class="form-row"><div><label>Email</label><input id="m-email"></div>' +
'<div><label>Город</label><input id="m-city"></div></div>' +
'<div class="form-row"><div><label>Рабочий телефон</label><input id="m-workphone"></div>' +
'<div><label>Домашний телефон</label><input id="m-homephone"></div>' +
'<div><label>Сотовый телефон</label><input id="m-mobilephone"></div></div>' +
'<div class="form-row"><div><label>IP адрес</label><input id="m-ipaddress"></div></div>' +
'<div class="form-row"><div><label>Роль</label><select id="m-role" onchange="togglePermissions()"><option value="operator">Оператор (только просмотр)</option><option value="moderator">Модератор (настраиваемые права)</option><option value="admin">Админ (полный доступ)</option></select></div></div>' +
'<div id="permissions-box" style="display: none; margin-top: 16px; background: var(--bg-tertiary); padding: 16px; border-radius: 8px;">' +
'<label style="display: block; margin-bottom: 12px;"><strong>Права доступа:</strong></label>' +
'<div style="margin-bottom:12px;"><strong>Задачи:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-todos-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-todos-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-todos-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-todos-delete"> Удаление</label>' +
'<div style="margin:12px 0 4px;"><strong>Системы:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-systems-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-systems-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-systems-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-systems-delete"> Удаление</label>' +
'<div style="margin:12px 0 4px;"><strong>Серверы:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-servers-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-servers-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-servers-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-servers-delete"> Удаление</label>' +
'<div style="margin:12px 0 4px;"><strong>Админы:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-admins-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-admins-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-admins-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-admins-delete"> Удаление</label>' +
'<div style="margin:12px 0 4px;"><strong>Операторы:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-users-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-users-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-users-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-users-delete"> Удаление</label>' +
'<div style="margin:12px 0 4px;"><strong>Распределение:</strong></div>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-distribution-view"> Просмотр</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-distribution-create"> Создание</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-distribution-edit"> Редактирование</label>' +
'<label style="display: block; margin: 4px 0;"><input type="checkbox" id="perm-distribution-delete"> Удаление</label>' +
'</div>',
() => {
const role = document.getElementById('m-role').value;
const permissions = role === 'moderator' ? {
todos: { view: document.getElementById('perm-todos-view').checked, create: document.getElementById('perm-todos-create').checked, edit: document.getElementById('perm-todos-edit').checked, delete: document.getElementById('perm-todos-delete').checked },
systems: { view: document.getElementById('perm-systems-view').checked, create: document.getElementById('perm-systems-create').checked, edit: document.getElementById('perm-systems-edit').checked, delete: document.getElementById('perm-systems-delete').checked },
servers: { view: document.getElementById('perm-servers-view').checked, create: document.getElementById('perm-servers-create').checked, edit: document.getElementById('perm-servers-edit').checked, delete: document.getElementById('perm-servers-delete').checked },
admins: { view: document.getElementById('perm-admins-view').checked, create: document.getElementById('perm-admins-create').checked, edit: document.getElementById('perm-admins-edit').checked, delete: document.getElementById('perm-admins-delete').checked },
users: { view: document.getElementById('perm-users-view').checked, create: document.getElementById('perm-users-create').checked, edit: document.getElementById('perm-users-edit').checked, delete: document.getElementById('perm-users-delete').checked },
distribution: { view: document.getElementById('perm-distribution-view').checked, create: document.getElementById('perm-distribution-create').checked, edit: document.getElementById('perm-distribution-edit').checked, delete: document.getElementById('perm-distribution-delete').checked }
} : {};
const item = {
login: document.getElementById('m-login').value,
password: document.getElementById('m-password').value,
name: document.getElementById('m-name').value,
position: document.getElementById('m-position').value,
email: document.getElementById('m-email').value,
city: document.getElementById('m-city').value,
workPhone: document.getElementById('m-workphone').value,
homePhone: document.getElementById('m-homephone').value,
mobilePhone: document.getElementById('m-mobilephone').value,
ipAddress: document.getElementById('m-ipaddress').value,
role,
permissions
};
fetch('api/app-users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify(item) }).then(() => { closeModal(); loadAll(); renderAppUsers(); });
}
);
}
function editAppUser(login) {
editId = login;
const u = appUsers.find(x => x.login === login);
// Поддержка старых и новых полей
const workPhone = u.workPhone || u.phone || '';
const homePhone = u.homePhone || '';
const mobilePhone = u.mobilePhone || '';
const ipAddress = u.ipAddress || u.dbAddress || '';
openModal('Редактировать пользователя',
'<div class="form-row"><div><label>Логин</label><input id="m-login" value="' + (u.login || '') + '"></div>' +
'<div><label>ФИО</label><input id="m-name" value="' + (u.name || '') + '"></div></div>' +
'<div><label>Должность</label><input id="m-position" value="' + (u.position || '') + '"></div></div>' +
'<div class="form-row"><div><label>Email</label><input id="m-email" value="' + (u.email || '') + '"></div>' +
'<div><label>Город</label><input id="m-city" value="' + (u.city || '') + '"></div></div>' +
'<div class="form-row"><div><label>Рабочий телефон</label><input id="m-workphone" value="' + workPhone + '"></div>' +
'<div><label>Домашний телефон</label><input id="m-homephone" value="' + homePhone + '"></div>' +
'<div><label>Сотовый телефон</label><input id="m-mobilephone" value="' + mobilePhone + '"></div></div>' +
'<div class="form-row"><div><label>IP адрес</label><input id="m-ipaddress" value="' + ipAddress + '"></div></div>' +
'<div class="form-row" style="margin-top: 16px;"><div><label>Роль</label><select id="m-role"><option value="operator"' + (u.role === 'operator' ? ' selected' : '') + '>Оператор (только просмотр)</option><option value="moderator"' + (u.role === 'moderator' ? ' selected' : '') + '>Модератор (настраиваемые права)</option><option value="admin"' + (u.role === 'admin' ? ' selected' : '') + '>Админ (полный доступ)</option></select></div></div>' +
'<div class="form-row" style="margin-top: 16px;"><div><label>Новый пароль (оставь пустым, чтобы не менять)</label><input type="password" id="m-password"></div></div>',
() => {
const newLogin = document.getElementById('m-login').value;
const oldLogin = login;
if (newLogin !== oldLogin && appUsers.find(x => x.login === newLogin)) {
alert('Пользователь с таким логином уже существует!');
return;
}
const upd = {
login: newLogin,
name: document.getElementById('m-name').value,
position: document.getElementById('m-position').value,
email: document.getElementById('m-email').value,
city: document.getElementById('m-city').value,
workPhone: document.getElementById('m-workphone').value,
homePhone: document.getElementById('m-homephone').value,
mobilePhone: document.getElementById('m-mobilephone').value,
ipAddress: document.getElementById('m-ipaddress').value,
role: document.getElementById('m-role').value
};
const pwd = document.getElementById('m-password').value;
if (pwd) upd.password = pwd;
if (newLogin !== oldLogin) {
// Удаляем старого и создаём нового
fetch('api/app-users/' + encodeURIComponent(oldLogin), { method: 'DELETE', headers: { 'X-User': currentUser.login } })
.then(() => fetch('api/app-users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify(upd) }))
.then(() => { closeModal(); loadAll(); renderAppUsers(); });
} else {
fetch('api/app-users/' + encodeURIComponent(oldLogin), { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify(upd) }).then(() => { closeModal(); loadAll(); renderAppUsers(); });
}
}
);
}
function editAppUserPermissions(login) {
const u = appUsers.find(x => x.login === login);
const p = u.permissions || {};
openModal('Права доступа: ' + (u.name || login),
'<div><label>Описание</label><input id="perm-user-desc" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);" value="Настройка прав для пользователя ' + (u.name || login) + '" readonly></div>' +
'<div style="margin-top:16px;"><strong>Права доступа:</strong></div>' +
'<div id="role-permissions-box" style="margin-top:8px;"></div>',
() => {
const perms = collectRolePermissions();
fetch('api/app-users/' + encodeURIComponent(login), { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ permissions: perms }) })
.then(() => { closeModal(); loadAll(); renderAppUsers(); });
},
() => { renderRolePermissions(p || {}); }
);
}
function deleteAppUser(login) { if (confirm('Удалить пользователя?')) { fetch('api/app-users/' + encodeURIComponent(login), { method: 'DELETE', headers: { 'X-User': currentUser.login } }).then(() => { loadAll(); renderAppUsers(); }); } }
// AUDIT LOG
function renderRoles() {
if (currentUser.role !== 'admin') return;
document.getElementById('roles-body').innerHTML = (roles || []).map(r =>
'<tr><td>' + (r.name || '—') + '</td>' +
'<td>' + (r.description || '—') + '</td>' +
'<td class="actions">' +
(r.name !== 'admin' ? '<button class="btn btn-sm btn-secondary" onclick="editRole(\'' + r.name + '\')">✏</button>' : '') +
(r.name !== 'admin' ? '<button class="btn btn-sm btn-danger" onclick="deleteRole(\'' + r.name + '\')">✕</button>' : '') +
'</td></tr>'
).join('') || '<tr><td colspan="3" style="text-align: center; color: var(--text-secondary);">Нет ролей</td></tr>';
}
function addRole() {
editId = null;
openModal('Добавить роль',
'<div class="form-row"><div><label>Название</label><input id="m-role-name"></div></div>' +
'<div><label>Описание</label><input id="m-role-desc" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);"></div>' +
'<div style="margin-top:16px;"><strong>Права доступа:</strong></div>' +
'<div id="role-permissions-box" style="margin-top:8px;"></div>',
() => {
const name = document.getElementById('m-role-name').value;
const description = document.getElementById('m-role-desc').value;
const permissions = collectRolePermissions();
if (!name) { alert('Введите название'); return; }
fetch('api/roles', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ name, description, permissions }) })
.then(() => { closeModal(); loadAll(); });
},
() => { renderRolePermissions({}); }
);
}
function editRole(name) {
editId = name;
const r = roles.find(x => x.name === name);
if (!r) return;
openModal('Редактировать роль: ' + name,
'<div class="form-row"><div><label>Название</label><input id="m-role-name" value="' + r.name + '"></div></div>' +
'<div><label>Описание</label><input id="m-role-desc" style="width:100%;padding:10px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);" value="' + (r.description || '') + '"></div>' +
'<div style="margin-top:16px;"><strong>Права доступа:</strong></div>' +
'<div id="role-permissions-box" style="margin-top:8px;"></div>',
() => {
const newName = document.getElementById('m-role-name').value;
const description = document.getElementById('m-role-desc').value;
const permissions = collectRolePermissions();
if (!newName) { alert('Введите название'); return; }
fetch('api/roles/' + encodeURIComponent(name), { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-User': currentUser.login }, body: JSON.stringify({ name: newName, description, permissions }) })
.then(() => { closeModal(); loadAll(); });
},
() => { renderRolePermissions(r.permissions || {}); }
);
}
function renderRolePermissions(perms) {
const sections = ['todos', 'systems', 'servers', 'admins', 'operators', 'distribution'];
const actions = ['view', 'create', 'edit', 'delete'];
const labels = { view: 'Просмотр', create: 'Создание', edit: 'Редактирование', delete: 'Удаление' };
const sectionNames = { todos: 'Задачи', systems: 'Системы', servers: 'Серверы', admins: 'Админы', operators: 'Операторы', distribution: 'Распределение' };
const p = perms || {};
let html = '';
sections.forEach(sec => {
html += '<div style="background:var(--bg-tertiary);padding:12px;border-radius:8px;margin-bottom:8px;">';
html += '<strong>' + (sectionNames[sec] || sec) + ':</strong><br>';
actions.forEach(act => {
const checked = (p[sec] && p[sec][act]) ? ' checked' : '';
html += '<label style="display:inline-block;margin:4px 8px 4px 0;"><input type="checkbox" data-perm="' + sec + '-' + act + '"' + checked + '> ' + labels[act] + '</label>';
});
html += '</div>';
});
document.getElementById('role-permissions-box').innerHTML = html;
}
function collectRolePermissions() {
const sections = ['todos', 'systems', 'servers', 'admins', 'operators', 'distribution'];
const actions = ['view', 'create', 'edit', 'delete'];
const perms = {};
sections.forEach(sec => {
perms[sec] = {};
actions.forEach(act => {
const cb = document.querySelector('input[data-perm="' + sec + '-' + act + '"]');
perms[sec][act] = cb ? cb.checked : false;
});
});
return perms;
}
function deleteRole(name) {
if (confirm('Удалить роль "' + name + '"? Все пользователи с этой ролью получат роль "operator".')) {
fetch('api/roles/' + encodeURIComponent(name), { method: 'DELETE', headers: { 'X-User': currentUser.login } })
.then(() => { loadAll(); });
}
}
function renderAudit() {
const search = document.getElementById('audit-search').value.toLowerCase();
const userFilter = document.getElementById('audit-user').value;
let filtered = auditLog.filter(a =>
(a.user || '').toLowerCase().includes(search) ||
(a.action || '').toLowerCase().includes(search) ||
(a.details || '').toLowerCase().includes(search)
);
if (userFilter) filtered = filtered.filter(a => a.user === userFilter);
document.getElementById('audit-body').innerHTML = filtered.map(a =>
'<tr><td>' + (a.when || '—').replace('T', ' ').slice(0, 16) + '</td>' +
'<td>' + (a.user || '—') + '</td>' +
'<td>' + (a.action || '—') + '</td>' +
'<td>' + (a.details || '—') + '</td></tr>'
).join('') || '<tr><td colspan="4" style="text-align: center; color: var(--text-secondary);">Нет записей</td></tr>';
// Populate user filter
const users = [...new Set(auditLog.map(a => a.user))];
const sel = document.getElementById('audit-user');
if (sel.options.length === 1) {
users.forEach(u => {
const opt = document.createElement('option');
opt.value = u;
opt.textContent = u;
sel.appendChild(opt);
});
}
}
// PROFILE
async function loadProfile() {
try {
const headers = { 'X-User': currentUser.login };
const profile = await fetch('api/profile', { headers }).then(r => r.json());
document.getElementById('profile-login').value = profile.login || '';
document.getElementById('profile-name').value = profile.name || '';
document.getElementById('profile-position').value = profile.position || '';
document.getElementById('profile-email').value = profile.email || '';
document.getElementById('profile-workphone').value = profile.workPhone || '';
document.getElementById('profile-homephone').value = profile.homePhone || '';
document.getElementById('profile-mobilephone').value = profile.mobilePhone || '';
document.getElementById('profile-city').value = profile.city || '';
document.getElementById('profile-ipaddress').value = profile.ipAddress || '';
document.getElementById('profile-password').value = profile.password || '';
// Также обновим currentUser чтобы данные были актуальны
currentUser.name = profile.name;
currentUser.position = profile.position;
document.getElementById('user-info').textContent = currentUser.name + ' (' + (currentUser.role === 'admin' ? 'Админ' : currentUser.role === 'moderator' ? 'Модератор' : 'Оператор') + ')';
} catch (e) {
console.error('Failed to load profile:', e);
}
}
async function saveProfile() {
const name = document.getElementById('profile-name').value.trim();
const position = document.getElementById('profile-position').value.trim();
const password = document.getElementById('profile-password').value.trim();
if (!name || !position || !password) {
alert('Поля Логин, ФИО, Должность и Пароль обязательны для заполнения');
return;
}
try {
const headers = { 'X-User': currentUser.login, 'Content-Type': 'application/json' };
const body = {
name: name,
position: position,
email: document.getElementById('profile-email').value.trim(),
workPhone: document.getElementById('profile-workphone').value.trim(),
homePhone: document.getElementById('profile-homephone').value.trim(),
mobilePhone: document.getElementById('profile-mobilephone').value.trim(),
city: document.getElementById('profile-city').value.trim(),
ipAddress: document.getElementById('profile-ipaddress').value.trim(),
password: password
};
const res = await fetch('api/profile', { method: 'PUT', headers, body: JSON.stringify(body) });
const result = await res.json();
if (result.ok) {
alert('Профиль сохранён успешно!');
loadProfile();
} else {
alert('Ошибка сохранения: ' + (result.error || 'Неизвестная ошибка'));
}
} catch (e) {
alert('Ошибка сохранения: ' + e.message);
}
}
checkAuth();
</script>
</body>
</html>