1974 lines
110 KiB
HTML
1974 lines
110 KiB
HTML
<!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>
|