Add audit details modal and translate remaining action labels

- Make Детали cell clickable to open a modal with the full details text\n- Translate missing audit action keys (auth.login, user.*, branch.*, group.photo.*, entry.photo.*)
This commit is contained in:
dev
2026-09-11 14:45:07 +03:00
parent 7044505915
commit ee19da7fa0
3 changed files with 53 additions and 2 deletions
+4
View File
@@ -353,6 +353,10 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
.audit-table tr:last-child td{border-bottom:none} .audit-table tr:last-child td{border-bottom:none}
.audit-table tr:hover td{background:rgba(37,99,235,.04)} .audit-table tr:hover td{background:rgba(37,99,235,.04)}
.audit-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto} .audit-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);overflow-x:auto}
.audit-details{background:none;border:none;padding:0;margin:0;color:var(--accent);font-size:.8rem;text-align:left;cursor:pointer;max-width:360px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.audit-details:hover{text-decoration:underline}
.audit-details-body{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;max-height:60vh;overflow:auto}
.audit-details-body pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.78rem;line-height:1.45;color:var(--text)}
/* --- AI Correction --- */ /* --- AI Correction --- */
.ai-btn{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;padding:4px 8px;border-radius:6px;transition:color .15s,background .15s;vertical-align:middle;margin-left:6px} .ai-btn{background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.1rem;padding:4px 8px;border-radius:6px;transition:color .15s,background .15s;vertical-align:middle;margin-left:6px}
+9
View File
@@ -38,6 +38,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal-overlay" id="auditDetailsModal">
<div class="edit-modal" style="max-width:640px">
<h3 id="auditDetailsTitle">Детали действия</h3>
<div class="audit-details-body"><pre id="auditDetailsBody"></pre></div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="cancel" id="auditDetailsCloseBtn">Закрыть</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script src="vendor/lucide.min.js"></script> <script src="vendor/lucide.min.js"></script>
<script src="admin.js"></script> <script src="admin.js"></script>
+40 -2
View File
@@ -1,16 +1,28 @@
const ACTION_LABELS = { const ACTION_LABELS = {
'auth.login': 'Вход в систему',
'settings.update': 'Изменены настройки', 'settings.update': 'Изменены настройки',
'user.create': 'Создан пользователь',
'user.update': 'Изменён пользователь',
'user.delete': 'Удалён пользователь',
'group.create': 'Создана группа', 'group.create': 'Создана группа',
'group.update': 'Изменена группа', 'group.update': 'Изменена группа',
'group.delete': 'Удалена группа', 'group.delete': 'Удалена группа',
'group.photo.create': 'Загружена фото группы', 'group.photo.create': 'Загружена фото группы',
'group.photo.update': 'Изменено фото группы', 'group.photo.update': 'Изменено фото группы',
'group.photo.reorder': 'Изменён порядок фото группы',
'group.photo.set_cover': 'Обновлена обложка группы',
'group.photo.delete': 'Удалено фото группы', 'group.photo.delete': 'Удалено фото группы',
'branch.create': 'Создан филиал',
'branch.update': 'Изменён филиал',
'branch.delete': 'Удалён филиал',
'student.create': 'Добавлен ученик', 'student.create': 'Добавлен ученик',
'student.update': 'Изменён ученик', 'student.update': 'Изменён ученик',
'student.delete': 'Удалён ученик', 'student.delete': 'Удалён ученик',
'student.batch-group': 'Ученики перенесены в группу', 'student.batch-group': 'Ученики перенесены в группу',
'entry.update': 'Изменена запись', 'entry.update': 'Изменена запись',
'entry.photo.update': 'Изменено фото записи',
'entry.photo.set_main': 'Главное фото записи',
'entry.photo.delete': 'Удалено фото записи',
'entry.soft-delete': 'Запись в корзину', 'entry.soft-delete': 'Запись в корзину',
'entry.restore': 'Запись восстановлена', 'entry.restore': 'Запись восстановлена',
'entry.permanent-delete': 'Запись удалена безвозвратно', 'entry.permanent-delete': 'Запись удалена безвозвратно',
@@ -32,6 +44,7 @@ const ACTION_LABELS = {
const AUDIT_PAGE_SIZE = 50; const AUDIT_PAGE_SIZE = 50;
let auditPage = 1; let auditPage = 1;
let auditTotal = 0; let auditTotal = 0;
let auditRows = [];
function fmtTime(t) { function fmtTime(t) {
try { return new Date(t).toLocaleString('ru'); } catch { return t || ''; } try { return new Date(t).toLocaleString('ru'); } catch { return t || ''; }
@@ -48,6 +61,14 @@ function dt(t) {
} catch { return esc(String(t).slice(0, 120)); } } catch { return esc(String(t).slice(0, 120)); }
} }
function fullTarget(t) {
if (!t) return '';
if (typeof t === 'object') return JSON.stringify(t, null, 2);
try {
return JSON.stringify(JSON.parse(t), null, 2);
} catch { return String(t); }
}
function renderPager(container, page, totalPages, onPageChange) { function renderPager(container, page, totalPages, onPageChange) {
if (totalPages <= 1) { container.innerHTML = ''; return; } if (totalPages <= 1) { container.innerHTML = ''; return; }
const pages = []; const pages = [];
@@ -86,11 +107,12 @@ async function loadAudit() {
el.innerHTML = ''; el.innerHTML = '';
} else { } else {
document.getElementById('auditEmpty').style.display = 'none'; document.getElementById('auditEmpty').style.display = 'none';
el.innerHTML = filtered.map(r => ` auditRows = filtered;
el.innerHTML = filtered.map((r, i) => `
<tr> <tr>
<td style="white-space:nowrap">${fmtTime(r.created_at)}</td> <td style="white-space:nowrap">${fmtTime(r.created_at)}</td>
<td><b>${esc(ACTION_LABELS[r.action] || r.action)}</b></td> <td><b>${esc(ACTION_LABELS[r.action] || r.action)}</b></td>
<td style="color:var(--muted)">${dt(r.target)}</td> <td><button type="button" class="audit-details" data-audit-idx="${i}">${dt(r.target)}</button></td>
<td style="color:var(--muted);white-space:nowrap">${esc(r.ip || '')}</td> <td style="color:var(--muted);white-space:nowrap">${esc(r.ip || '')}</td>
</tr>`).join(''); </tr>`).join('');
} }
@@ -105,6 +127,22 @@ function goAuditPage(p) {
document.getElementById('auditFilter').addEventListener('input', () => { auditPage = 1; loadAudit(); }); document.getElementById('auditFilter').addEventListener('input', () => { auditPage = 1; loadAudit(); });
document.getElementById('auditRefreshBtn').addEventListener('click', loadAudit); document.getElementById('auditRefreshBtn').addEventListener('click', loadAudit);
document.getElementById('auditTable').addEventListener('click', e => {
const btn = e.target.closest('.audit-details');
if (!btn) return;
const r = auditRows[parseInt(btn.dataset.auditIdx, 10)];
if (!r) return;
document.getElementById('auditDetailsTitle').textContent = ACTION_LABELS[r.action] || r.action;
document.getElementById('auditDetailsBody').textContent = fullTarget(r.target);
document.getElementById('auditDetailsModal').classList.add('open');
});
document.getElementById('auditDetailsCloseBtn').addEventListener('click', () => {
document.getElementById('auditDetailsModal').classList.remove('open');
});
document.getElementById('auditDetailsModal').addEventListener('click', e => {
if (e.target === e.currentTarget) e.currentTarget.classList.remove('open');
});
(async () => { (async () => {
if (await requireAdminPage()) { if (await requireAdminPage()) {
buildSidebar(document.body.dataset.page); buildSidebar(document.body.dataset.page);