feat: UI — edit modal, admin-панель выбора компании, кнопки Аудит/Лимиты; views/audit.ejs, views/admin.ejs
This commit is contained in:
+158
-4
@@ -157,6 +157,14 @@
|
||||
</svg>
|
||||
<span>|</span>
|
||||
<span style="font-weight:500;color:var(--text);">Белые списки IP</span>
|
||||
<!-- Кнопки правой части header: admin-ссылки + выход -->
|
||||
<span style="margin-left:auto;display:flex;gap:.5rem;align-items:center;">
|
||||
<% if (isAdmin) { %>
|
||||
<a href="/audit" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Аудит</a>
|
||||
<a href="/admin" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Лимиты</a>
|
||||
<% } %>
|
||||
<a href="/logout" style="font-size:.8rem;color:var(--muted);text-decoration:none;padding:.2rem .6rem;border:1px solid var(--border);border-radius:5px;background:#fff;">Выйти (<%= user.email %>)</a>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="page">
|
||||
@@ -164,6 +172,40 @@
|
||||
<% if (message) { %><div class="alert <%= wasNormalized ? 'alert-warn' : 'alert-ok' %>"><%= message %></div><% } %>
|
||||
<% if (error) { %><div class="alert alert-err"><%= error %></div><% } %>
|
||||
|
||||
<%
|
||||
// Для admin: selectedCompany — выбранная компания (объект из getAllCompanies).
|
||||
// Для обычного пользователя: selectedCompany — их компания (из getOrCreateCompany).
|
||||
// company_id — числовой PK для передачи в скрытые поля форм.
|
||||
const companyPk = selectedCompany ? selectedCompany.id : null;
|
||||
%>
|
||||
|
||||
<!-- Переключатель компании — только для администратора -->
|
||||
<% if (isAdmin && companies) { %>
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:.75rem 1rem;">
|
||||
<form method="GET" action="/" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
|
||||
<span style="font-size:.85rem;color:var(--muted);font-weight:500;">Компания:</span>
|
||||
<select name="company" onchange="this.form.submit()"
|
||||
style="padding:.4rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.85rem;background:#fff;">
|
||||
<% companies.forEach(c => { %>
|
||||
<option value="<%= c.id %>" <%= selectedCompany && c.id === selectedCompany.id ? 'selected' : '' %>>
|
||||
<%= c.name %> (<%= c.client_id %>) — <%= c.active_count %> зап.
|
||||
</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<!-- Быстрый доступ к admin-страницам прямо из главной -->
|
||||
<a href="/audit<%= selectedCompany ? '?company=' + selectedCompany.id : '' %>"
|
||||
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Аудит этой компании</a>
|
||||
<a href="/admin"
|
||||
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Лимиты</a>
|
||||
<span style="margin-left:auto;font-size:.8rem;color:var(--muted);">
|
||||
Режим: <strong style="color:var(--amber);">Администратор</strong>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Статистика -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@@ -173,7 +215,8 @@
|
||||
<div class="lbl">записей</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="num"><%= user.clientId %></div>
|
||||
<!-- Для admin показываем clientId выбранной компании, для user — их собственный -->
|
||||
<div class="num"><%= selectedCompany ? selectedCompany.client_id : user.clientId %></div>
|
||||
<div class="lbl">компания</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
@@ -189,6 +232,13 @@
|
||||
<div class="card-header">Добавить адрес</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/add">
|
||||
<%
|
||||
// Передаём числовой PK компании в скрытом поле.
|
||||
// Для admin: server.js возьмёт его из req.body.company_id.
|
||||
// Для обычного пользователя: server.js игнорирует это поле
|
||||
// и использует clientId из токена — защита от подмены.
|
||||
%>
|
||||
<input type="hidden" name="company_id" value="<%= companyPk || '' %>">
|
||||
<div class="form-grid">
|
||||
<div class="field">
|
||||
<label>IPv4 адрес или подсеть CIDR</label>
|
||||
@@ -220,7 +270,8 @@
|
||||
<th>Комментарий</th>
|
||||
<th>Добавил</th>
|
||||
<th>Дата</th>
|
||||
<th class="text-center">Действие</th>
|
||||
<th>Изменён</th>
|
||||
<th class="text-center">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -230,8 +281,33 @@
|
||||
<td><%= e.comment || '—' %></td>
|
||||
<td><%= e.created_by %></td>
|
||||
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}) %></td>
|
||||
<td class="text-center">
|
||||
<form method="POST" action="/delete/<%= e.id %>" style="display:inline" onsubmit="return confirm('Удалить запись?')">
|
||||
<!-- Дата последнего редактирования (null если не менялась) -->
|
||||
<td style="color:var(--muted);font-size:.8rem;">
|
||||
<%= e.updated_at
|
||||
? new Date(e.updated_at).toLocaleDateString('ru', {day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'})
|
||||
: '—' %>
|
||||
</td>
|
||||
<td class="text-center" style="white-space:nowrap;">
|
||||
<%
|
||||
// data-* атрибуты для JS-заполнения modal без inline-onclick с параметрами.
|
||||
// Так как комментарий может содержать кавычки — EJS экранирует их в HTML.
|
||||
// Но в data-атрибуте JSON::stringify безопаснее.
|
||||
%>
|
||||
<!-- Кнопка редактирования: открывает modal и заполняет его данными из data-* -->
|
||||
<button class="btn"
|
||||
data-id="<%= e.id %>"
|
||||
data-value="<%= e.value_cidr %>"
|
||||
data-comment="<%= e.comment || '' %>"
|
||||
data-company="<%= companyPk || '' %>"
|
||||
onclick="openEditModal(this)"
|
||||
style="margin-right:.25rem;">
|
||||
Изменить
|
||||
</button>
|
||||
<!-- Форма удаления: company_id нужен для admin-ветки в server.js -->
|
||||
<form method="POST" action="/delete/<%= e.id %>"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('Удалить запись <%= e.value_cidr %>?')">
|
||||
<input type="hidden" name="company_id" value="<%= companyPk || '' %>">
|
||||
<button class="btn btn-danger">Удалить</button>
|
||||
</form>
|
||||
</td>
|
||||
@@ -244,5 +320,83 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ══ Modal редактирования записи ══════════════════════════════════════════ -->
|
||||
<!--
|
||||
Модальное окно — фиксированный overlay поверх страницы.
|
||||
Открывается кнопкой "Изменить" в таблице (через JS openEditModal).
|
||||
Закрывается кнопкой "Отмена", кликом по backdrop или Escape.
|
||||
Данные заполняются из data-* атрибутов кнопки — никаких лишних fetch-запросов.
|
||||
-->
|
||||
<div id="edit-modal"
|
||||
style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;
|
||||
background:rgba(0,0,0,.4);z-index:200;align-items:center;justify-content:center;">
|
||||
<div style="background:#fff;border-radius:12px;padding:1.5rem;width:500px;
|
||||
max-width:92vw;box-shadow:0 8px 32px rgba(0,0,0,.18);">
|
||||
<h3 style="font-size:1rem;font-weight:600;margin-bottom:1.25rem;">Редактировать запись</h3>
|
||||
<form method="POST" id="edit-form">
|
||||
<!-- Скрытые поля: action (action='/edit/N') и company_id заполняет openEditModal() -->
|
||||
<input type="hidden" id="edit-company-id" name="company_id" value="">
|
||||
|
||||
<div class="field" style="margin-bottom:.75rem;">
|
||||
<label>IPv4 адрес или подсеть CIDR</label>
|
||||
<input id="edit-value" name="value"
|
||||
pattern="^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$"
|
||||
maxlength="18"
|
||||
title="IPv4 адрес или CIDR /22–/32"
|
||||
required
|
||||
style="padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;">
|
||||
</div>
|
||||
<div class="field" style="margin-bottom:1.25rem;">
|
||||
<label>Комментарий</label>
|
||||
<input id="edit-comment" name="comment"
|
||||
maxlength="255"
|
||||
placeholder="Необязательно"
|
||||
style="padding:.5rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;">
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;justify-content:flex-end;">
|
||||
<button type="button" class="btn" onclick="closeEditModal()">Отмена</button>
|
||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* Открыть modal редактирования.
|
||||
* Вызывается из onclick кнопки "Изменить" в таблице.
|
||||
* @param {HTMLElement} btn — кнопка с data-id, data-value, data-comment, data-company
|
||||
*/
|
||||
function openEditModal(btn) {
|
||||
// Заполняем поля из data-атрибутов (EJS уже HTML-экранировал значения)
|
||||
document.getElementById('edit-value').value = btn.dataset.value;
|
||||
document.getElementById('edit-comment').value = btn.dataset.comment;
|
||||
document.getElementById('edit-company-id').value = btn.dataset.company;
|
||||
// Устанавливаем action формы на конкретный /edit/:id
|
||||
document.getElementById('edit-form').action = '/edit/' + btn.dataset.id;
|
||||
// Показываем modal (display:flex для центрирования)
|
||||
const modal = document.getElementById('edit-modal');
|
||||
modal.style.display = 'flex';
|
||||
// Фокус на поле ввода для удобства клавиатурных пользователей
|
||||
setTimeout(() => document.getElementById('edit-value').focus(), 50);
|
||||
}
|
||||
|
||||
/** Закрыть modal без сохранения */
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Закрытие кликом по backdrop (за пределами карточки)
|
||||
document.getElementById('edit-modal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeEditModal();
|
||||
});
|
||||
|
||||
// Закрытие клавишей Escape
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeEditModal();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user