Files
sts-avto-v2/public/admin.html
kalugin66 0b61d5be82
All checks were successful
Deploy sts / deploy-sber (push) Successful in 1m12s
фикс край
2026-08-03 23:14:14 +05:00

1016 lines
57 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Админ-панель | СТС-Авто</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&family=Roboto+Condensed:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
:root{--dark:#1a2a3a;--dark-deep:#0d1a27;--bg:#f5f2ed;--bg-white:#fff;--orange:#E67E22;--orange-dark:#D35400;--orange-light:#F39C12;--text:#1a2a3a;--text-gray:#5a6a7a;--border:#d5d0c8;--danger:#e74c3c;--success:#27AE60;--sidebar-w:260px}
body{font-family:'Roboto Condensed',sans-serif;color:var(--text);background:var(--bg);display:flex;min-height:100vh}
#login-page{display:flex;align-items:center;justify-content:center;width:100%;min-height:100vh;background:linear-gradient(135deg,#1a2a3a,#2c3e50)}
.login-box{background:#fff;border-radius:16px;padding:40px 36px;width:100%;max-width:400px;box-shadow:0 20px 60px rgba(0,0,0,.3)}
.login-box h2{font-family:'Montserrat',sans-serif;font-weight:800;font-size:1.6rem;margin-bottom:6px;color:var(--dark)}
.login-box p{color:var(--text-gray);margin-bottom:24px;font-size:.93rem}
.login-box input{width:100%;padding:12px 16px;border:1px solid var(--border);border-radius:8px;margin-bottom:14px;font-size:1rem;font-family:'Roboto Condensed',sans-serif;outline:none;transition:border .3s}
.login-box input:focus{border-color:var(--orange)}
.login-box .btn{width:100%;padding:14px;border:none;border-radius:8px;background:linear-gradient(135deg,var(--orange),var(--orange-dark));color:#fff;font-family:'Montserrat',sans-serif;font-weight:700;font-size:.95rem;text-transform:uppercase;letter-spacing:1px;cursor:pointer;transition:all .3s}
.login-box .btn:hover{transform:translateY(-2px);box-shadow:0 8px 25px rgba(230,126,34,.4)}
.login-error{color:var(--danger);font-size:.85rem;margin-top:10px;text-align:center;display:none}
#admin-panel{display:none;width:100%}
.sidebar{position:fixed;top:0;left:0;bottom:0;width:var(--sidebar-w);background:var(--dark);border-right:3px solid var(--orange);overflow-y:auto;z-index:100;padding-bottom:20px}
.sidebar-logo{padding:20px 20px 16px;border-bottom:1px solid rgba(255,255,255,.08);margin-bottom:12px}
.sidebar-logo h3{font-family:'Montserrat',sans-serif;font-weight:800;font-size:1.05rem;color:#fff}
.sidebar-logo span{color:var(--orange-light);font-size:.75rem;display:block;margin-top:2px}
.sidebar-nav{list-style:none;padding:0 10px}
.sidebar-nav li{margin-bottom:2px}
.sidebar-nav a{display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:8px;color:#bdc3c7;font-family:'Montserrat',sans-serif;font-weight:600;font-size:.82rem;text-transform:uppercase;letter-spacing:.5px;text-decoration:none;transition:all .3s;cursor:pointer}
.sidebar-nav a:hover{background:rgba(230,126,34,.1);color:#fff}
.sidebar-nav a.active{background:linear-gradient(135deg,var(--orange),var(--orange-dark));color:#fff}
.sidebar-nav a i{width:18px;text-align:center;font-size:.85rem}
.sidebar-logout{border-top:1px solid rgba(255,255,255,.08);margin-top:12px;padding-top:12px}
.main{flex:1;margin-left:var(--sidebar-w);padding:20px 24px;min-height:100vh}
.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-wrap:wrap;gap:12px}
.page-header h1{font-family:'Montserrat',sans-serif;font-weight:800;font-size:1.5rem}
.page-header h1 span{color:var(--orange)}
.btn{display:inline-flex;align-items:center;gap:6px;padding:10px 20px;border-radius:6px;font-family:'Montserrat',sans-serif;font-weight:600;font-size:.8rem;text-transform:uppercase;letter-spacing:.8px;cursor:pointer;border:none;transition:all .3s;text-decoration:none}
.btn-primary{background:linear-gradient(135deg,var(--orange),var(--orange-dark));color:#fff}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(230,126,34,.3)}
.btn-danger{background:var(--danger);color:#fff}
.btn-danger:hover{transform:translateY(-1px);box-shadow:0 4px 15px rgba(231,76,60,.3)}
.btn-sm{padding:6px 12px;font-size:.7rem;letter-spacing:.5px}
.btn-icon{background:none;border:none;cursor:pointer;padding:4px 6px;border-radius:4px;transition:all .2s;font-size:.9rem}
.btn-icon.edit{color:var(--orange)}
.btn-icon.delete{color:var(--danger)}
.table-wrap{background:#fff;border-radius:12px;border:1px solid var(--border);overflow:hidden}
table{width:100%;border-collapse:collapse}
th{background:var(--bg);padding:12px 16px;text-align:left;font-family:'Montserrat',sans-serif;font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.8px;color:var(--text-gray);border-bottom:2px solid var(--border)}
td{padding:10px 16px;border-bottom:1px solid var(--border);font-size:.9rem;color:var(--text)}
tr:last-child td{border-bottom:none}
tr:hover td{background:rgba(230,126,34,.03)}
td.actions{white-space:nowrap}
.text-muted{color:var(--text-gray);font-size:.82rem}
.badge{display:inline-block;padding:3px 10px;border-radius:50px;font-family:'Montserrat',sans-serif;font-weight:600;font-size:.7rem;text-transform:uppercase}
.badge-success{background:rgba(39,174,96,.1);color:var(--success);border:1px solid rgba(39,174,96,.3)}
.badge-warning{background:rgba(241,196,15,.1);color:#b7950b;border:1px solid rgba(241,196,15,.3)}
.pagination{display:flex;gap:6px;justify-content:center;padding:16px;align-items:center}
.pagination button{padding:6px 12px;border:1px solid var(--border);background:#fff;border-radius:4px;cursor:pointer;font-size:.85rem;transition:all .2s}
.pagination button:hover{background:var(--orange);color:#fff;border-color:var(--orange)}
.pagination button.active{background:var(--orange);color:#fff;border-color:var(--orange)}
.pagination span{color:var(--text-gray);font-size:.85rem}
.search-bar{margin-bottom:16px;display:flex;gap:10px}
.search-bar input{padding:10px 16px;border:1px solid var(--border);border-radius:8px;font-size:.9rem;font-family:'Roboto Condensed',sans-serif;outline:none;flex:1;transition:border .3s}
.search-bar input:focus{border-color:var(--orange)}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:1000;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all .3s}
.modal-overlay.open{opacity:1;visibility:visible}
.modal{background:#fff;border-radius:16px;padding:28px 30px;width:90%;max-width:600px;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.3);position:relative}
.modal h3{font-family:'Montserrat',sans-serif;font-weight:800;font-size:1.2rem;margin-bottom:20px;color:var(--dark)}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--text-gray);padding:4px 8px}
.modal-close:hover{color:var(--danger)}
.form-group2{margin-bottom:14px}
.form-group2 label{display:block;font-family:'Montserrat',sans-serif;font-weight:700;font-size:.75rem;text-transform:uppercase;letter-spacing:.8px;color:var(--text-gray);margin-bottom:5px}
.form-group2 input,.form-group2 textarea,.form-group2 select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:6px;font-size:.9rem;font-family:'Roboto Condensed',sans-serif;outline:none;transition:border .3s}
.form-group2 input:focus,.form-group2 textarea:focus,.form-group2 select:focus{border-color:var(--orange)}
.form-group2 textarea{resize:vertical;min-height:80px}
.form-group2 .help{font-size:.75rem;color:var(--text-gray);margin-top:3px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:20px;padding-top:16px;border-top:1px solid var(--border)}
.preview-img{max-width:200px;max-height:120px;border-radius:6px;margin:6px 0;border:1px solid var(--border)}
.toast-container{position:fixed;top:20px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:8px}
.toast{padding:14px 20px;border-radius:8px;color:#fff;font-size:.9rem;font-family:'Montserrat',sans-serif;font-weight:600;box-shadow:0 8px 25px rgba(0,0,0,.2);animation:slideIn .3s ease-out;max-width:380px}
.toast-success{background:var(--success)}
.toast-error{background:var(--danger)}
@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}
.settings-card{background:#fff;border:1px solid var(--border);border-radius:12px;padding:20px 22px}
.settings-card h3{font-family:'Montserrat',sans-serif;font-weight:700;font-size:.95rem;margin-bottom:14px;color:var(--orange-dark)}
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
.stat-card{background:#fff;border:1px solid var(--border);border-radius:12px;padding:20px 22px}
.stat-card .num{font-family:'Montserrat',sans-serif;font-weight:900;font-size:2rem;color:var(--orange)}
.stat-card .lbl{font-size:.85rem;color:var(--text-gray);margin-top:4px}
.stat-card i{font-size:1.4rem;color:var(--orange);margin-bottom:8px;display:block}
@media (max-width:768px){.sidebar{width:200px}.main{margin-left:200px}.settings-grid{grid-template-columns:1fr}.form-row{grid-template-columns:1fr}}
</style>
</head>
<body>
<div id="login-page">
<div class="login-box">
<h2>СТС-Авто</h2>
<p>Административная панель</p>
<input type="text" id="loginInput" placeholder="Логин">
<input type="password" id="passInput" placeholder="Пароль">
<button class="btn" onclick="doLogin()">Войти</button>
<div class="login-error" id="loginError">Неверный логин или пароль</div>
</div>
</div>
<div id="admin-panel">
<aside class="sidebar">
<div class="sidebar-logo"><h3>СТС-Авто</h3><span>Админ-панель</span></div>
<ul class="sidebar-nav">
<li><a data-tab="dashboard" class="active"><i class="fas fa-chart-pie"></i> Дашборд</a></li>
<li><a data-tab="hero"><i class="fas fa-image"></i> Hero</a></li>
<li><a data-tab="about"><i class="fas fa-building"></i> О компании</a></li>
<li><a data-tab="services"><i class="fas fa-tools"></i> Услуги</a></li>
<li><a data-tab="schemes"><i class="fas fa-handshake"></i> Схемы работы</a></li>
<li><a data-tab="process"><i class="fas fa-list-ol"></i> Этапы работы</a></li>
<li><a data-tab="equipment"><i class="fas fa-truck"></i> Спецтехника</a></li>
<li><a data-tab="geography"><i class="fas fa-map-marker-alt"></i> География</a></li>
<li><a data-tab="licenses"><i class="fas fa-certificate"></i> Лицензии</a></li>
<li><a data-tab="activities"><i class="fas fa-star"></i> Деятельность</a></li>
<li><a data-tab="partners"><i class="fas fa-handshake"></i> Партнёры</a></li>
<li><a data-tab="settings"><i class="fas fa-cog"></i> Настройки</a></li>
<li><a data-tab="leads"><i class="fas fa-envelope"></i> Заявки</a></li>
<li><a data-tab="users"><i class="fas fa-users"></i> Пользователи</a></li>
<li class="sidebar-logout"><a onclick="logout()" style="color:var(--danger)"><i class="fas fa-sign-out-alt"></i> Выход</a></li>
</ul>
</aside>
<div class="main" id="mainContent"></div>
</div>
<div class="toast-container" id="toastContainer"></div>
<div class="modal-overlay" id="modalOverlay"><div class="modal" id="modalContent"></div></div>
<script>
(function() {
'use strict';
var BASE = '/api/admin';
var token = localStorage.getItem('adminToken');
var user = null;
var currentTab = 'dashboard';
var _rows = []; // current rows data
var _table = ''; // current table name
var _columns = []; // current columns config
var _fields = []; // current form fields
var _uploadPrefix = ''; // upload API prefix
var _tabId = 0; // tab switch guard
var headings = {
dashboard:'Дашборд', hero:'Hero-секция', about:'О компании', services:'Услуги',
schemes:'Схемы работы', process:'Этапы работы', equipment:'Спецтехника',
geography:'География', licenses:'Лицензии', activities:'Общественная деятельность',
partners:'Партнёры', settings:'Настройки сайта', leads:'Заявки', users:'Пользователи'
};
if (token) { verifyAndEnter(); }
function verifyAndEnter() {
fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { if (!r.ok) throw new Error(); return r.json(); })
.then(function(u) { user = u; enterPanel(); })
.catch(function() { token = null; localStorage.removeItem('adminToken'); });
}
function doLogin() {
var login = document.getElementById('loginInput').value.trim();
var pass = document.getElementById('passInput').value.trim();
if (!login || !pass) return;
fetch('/api/auth/login', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ login: login, password: pass })
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.error) { document.getElementById('loginError').style.display='block'; return; }
token = d.token; user = d.user;
localStorage.setItem('adminToken', token);
enterPanel();
}).catch(function() { document.getElementById('loginError').style.display='block'; });
}
function h(title) {
return '<div class="page-header"><h1><span>' + headings[currentTab] + '</span></h1></div>';
}
function toast(msg, type) {
type = type || 'success';
var t = document.createElement('div');
t.className = 'toast toast-' + type;
t.textContent = msg;
document.getElementById('toastContainer').appendChild(t);
setTimeout(function() { t.remove(); }, 4000);
}
function api(url, opts) {
opts = opts || {};
return fetch(url, {
method: opts.method || 'GET',
headers: Object.assign({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, opts.headers || {}),
body: opts.body || undefined
}).then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); });
}
function openModal(title, renderFn) {
var overlay = document.getElementById('modalOverlay');
var content = document.getElementById('modalContent');
content.innerHTML = '<button class="modal-close" onclick="closeModal()">&times;</button><h3>' + title + '</h3>';
renderFn(content);
overlay.classList.add('open');
overlay.onclick = function(e) { if (e.target === overlay) closeModal(); };
}
function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
}
function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function rendCol(v, row, col) {
if (col.render) return col.render(v, row);
if (col.type === 'json' && v) {
try {
var arr = JSON.parse(v);
var s = arr.map(function(i) { return typeof i === 'string' ? i : (i.label + ': ' + i.value); }).join(', ');
return s.length > 80 ? s.substring(0, 77) + '...' : s;
} catch(e) { return String(v).substring(0, 60); }
}
return v != null ? String(v) : '';
}
function renderTable() {
if (!_rows || _rows.length === 0) return '<p class="text-muted">Нет данных</p>';
var html = '<div class="table-wrap"><table><thead><tr>';
_columns.forEach(function(c) { html += '<th>' + c.label + '</th>'; });
html += '<th>Действия</th></tr></thead><tbody>';
_rows.forEach(function(row) {
html += '<tr>';
_columns.forEach(function(c) { html += '<td>' + rendCol(row[c.field], row, c) + '</td>'; });
html += '<td class="actions">';
html += '<button class="btn-icon edit" data-action="edit" data-id="' + row.id + '"><i class="fas fa-edit"></i></button>';
html += '<button class="btn-icon delete" data-action="delete" data-id="' + row.id + '"><i class="fas fa-trash"></i></button>';
html += '</td></tr>';
});
html += '</tbody></table></div>';
return html;
}
function jsonEditor(target, initialValue) {
var val;
try { val = JSON.parse(initialValue || '[]'); } catch(e) { val = []; }
var html = '<div class="json-editor" style="display:flex;flex-direction:column;gap:6px">';
var isSpec = val.length > 0 && typeof val[0] === 'object';
if (isSpec) {
val.forEach(function(item) {
html += '<div style="display:flex;gap:6px"><input placeholder="Название" oninput="winUpdateJson(this)" value="' + esc(item.label||'') + '" style="flex:1"><input placeholder="Значение" oninput="winUpdateJson(this)" value="' + esc(item.value||'') + '" style="flex:1"><button type="button" class="btn btn-danger btn-sm" onclick="this.parentElement.remove();var ed=this.closest(\'.json-editor\');ed.querySelector(\'input[type=hidden]\').value=JSON.stringify(winCollectPairs(ed))">&times;</button></div>';
});
} else {
val.forEach(function(item) {
html += '<div style="display:flex;gap:6px"><input oninput="winUpdateJson(this)" value="' + esc(String(item)) + '" style="flex:1"><button type="button" class="btn btn-danger btn-sm" onclick="this.parentElement.remove();var ed=this.closest(\'.json-editor\');ed.querySelector(\'input[type=hidden]\').value=JSON.stringify(winCollectList(ed))">&times;</button></div>';
});
}
html += '<button type="button" class="btn btn-sm" onclick="winAddJsonRow(this)">+ Добавить</button>';
html += '<input type="hidden" id="' + target + '" value="' + esc(JSON.stringify(val)) + '"></div>';
return html;
}
window.winCollectPairs = function(editor) {
var inputs = editor.querySelectorAll('input:not([type=hidden])');
var arr = [];
for (var i = 0; i < inputs.length; i += 2) {
if (inputs[i].value || (inputs[i+1] && inputs[i+1].value)) {
arr.push({ label: inputs[i].value, value: inputs[i+1] ? inputs[i+1].value : '' });
}
}
return arr;
};
window.winCollectList = function(editor) {
var inputs = editor.querySelectorAll('input:not([type=hidden])');
var arr = [];
inputs.forEach(function(inp) { if (inp.value) arr.push(inp.value); });
return arr;
};
window.winUpdateJson = function(el) {
var editor = el.closest('.json-editor');
var hidden = editor.querySelector('input[type=hidden]');
if (editor.querySelector('input[placeholder="Значение"]')) {
hidden.value = JSON.stringify(window.winCollectPairs(editor));
} else {
hidden.value = JSON.stringify(window.winCollectList(editor));
}
};
window.winAddJsonRow = function(btn) {
var editor = btn.closest('.json-editor');
btn.insertAdjacentHTML('beforebegin',
editor.querySelector('input[placeholder="Значение"]')
? '<div style="display:flex;gap:6px"><input placeholder="Название" oninput="winUpdateJson(this)" style="flex:1"><input placeholder="Значение" oninput="winUpdateJson(this)" style="flex:1"><button type="button" class="btn btn-danger btn-sm" onclick="this.parentElement.remove();var ed=this.closest(\'.json-editor\');ed.querySelector(\'input[type=hidden]\').value=JSON.stringify(winCollectPairs(ed))">&times;</button></div>'
: '<div style="display:flex;gap:6px"><input oninput="winUpdateJson(this)" style="flex:1"><button type="button" class="btn btn-danger btn-sm" onclick="this.parentElement.remove();var ed=this.closest(\'.json-editor\');ed.querySelector(\'input[type=hidden]\').value=JSON.stringify(winCollectList(ed))">&times;</button></div>'
);
};
function buildForm(html, fields, rowData, isEdit, container) {
fields.forEach(function(f) {
var val = rowData && rowData[f.name] != null ? String(rowData[f.name]) : '';
if (f.type === 'textarea') {
html += '<div class="form-group2"><label>' + f.label + '</label><textarea name="' + f.name + '">' + esc(val) + '</textarea></div>';
} else if (f.type === 'json') {
html += '<div class="form-group2"><label>' + f.label + '</label>' + jsonEditor(f.name, val) + '</div>';
} else if (f.type === 'number') {
html += '<div class="form-group2"><label>' + f.label + '</label><input name="' + f.name + '" type="number" value="' + esc(val || (f['default'] != null ? String(f['default']) : '0')) + '"></div>';
} else if (f.type === 'image') {
var src = '';
if (val) {
if (val.startsWith('./')) src = val;
else if (val.startsWith('http')) src = val;
else if (val.startsWith('equip_') || val.startsWith('prt_') || val.startsWith('lic_') || val.startsWith('act_') || val.indexOf('/') !== -1) src = '/data/' + _uploadPrefix + '_images/' + val;
else src = './img/' + val;
}
html += '<div class="form-group2"><label>' + f.label + '</label><input name="' + f.name + '" value="' + esc(val) + '">';
if (src) html += '<div style="margin-top:6px"><img src="' + src + '" class="preview-img" onerror="this.style.display=\'none\'"></div>';
html += '<input type="file" onchange="winUploadFile(this)" style="margin-top:6px"><div class="help">Загрузите файл или введите путь</div></div>';
} else {
html += '<div class="form-group2"><label>' + f.label + '</label><input name="' + f.name + '" value="' + esc(val) + '"></div>';
}
});
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">' + (isEdit ? 'Сохранить' : 'Создать') + '</button></div>';
return html;
}
window.winUploadFile = function(input) {
var file = input.files[0];
if (!file) return;
var fd = new FormData();
fd.append('image', file);
fetch(BASE + '/' + _uploadPrefix + '/upload', {
method: 'POST', headers: { 'Authorization': 'Bearer ' + token }, body: fd
}).then(function(r) { return r.json(); }).then(function(d) {
if (d.path) {
var nameInput = input.parentElement.querySelector('input:not([type=file])');
if (nameInput) nameInput.value = d.path;
toast('Файл загружен');
} else toast('Ошибка загрузки', 'error');
});
};
function submitForm(formEl, id) {
var fd = new FormData(formEl);
var body = {};
fd.forEach(function(v, k) { body[k] = v; });
var url = id ? (BASE + '/' + _table + '/' + id) : (BASE + '/' + _table);
api(url, { method: id ? 'PUT' : 'POST', body: JSON.stringify(body) }).then(function(r) {
if (r.ok) { toast(id ? 'Сохранено' : 'Создано'); closeModal(); loadCurrentTab(); }
else toast(r.data.error, 'error');
});
}
function createItem() {
openModal('Создать', function(content) {
content.innerHTML = '<form id="_itemForm">' + buildForm('', _fields, null, false, content) + '</form>';
content.querySelector('#_itemForm').addEventListener('submit', function(e) {
e.preventDefault(); submitForm(this);
});
});
}
function editItem(id) {
var row = _rows.find(function(r) { return r.id === id; });
if (!row) return;
openModal('Редактировать', function(content) {
content.innerHTML = '<form id="_itemForm">' + buildForm('', _fields, row, true, content) + '</form>';
content.querySelector('#_itemForm').addEventListener('submit', function(e) {
e.preventDefault(); submitForm(this, id);
});
});
}
function deleteItem(id) {
if (!confirm('Удалить?')) return;
api(BASE + '/' + _table + '/' + id, { method: 'DELETE' }).then(function(r) {
if (r.ok) { toast('Удалено'); loadCurrentTab(); }
else toast(r.data.error, 'error');
});
}
function setupTableHandlers() {
document.getElementById('mainContent').addEventListener('click', function(e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
var action = btn.dataset.action;
var id = parseInt(btn.dataset.id);
if (action === 'edit') editItem(id);
else if (action === 'delete') deleteItem(id);
});
}
function loadCurrentTab() {
_tabId++;
currentTab = currentTab;
var fn = window['tab_' + currentTab];
if (fn) fn(_tabId);
}
function renderCrud() {
document.getElementById('mainContent').innerHTML = h() +
'<button class="btn btn-primary" style="margin-bottom:16px" onclick="createItem()"><i class="fas fa-plus"></i> Добавить</button>' +
renderTable();
}
function renderCrudAndLoad() {
renderCrud();
setupTableHandlers();
}
window.createItem = createItem;
window.editItem = editItem;
window.deleteItem = deleteItem;
window.closeModal = closeModal;
window.doLogin = doLogin;
window.logout = logout;
function configCrud(table, columns, fields, uploadPrefix) {
_table = table;
_columns = columns;
_fields = fields;
_uploadPrefix = uploadPrefix || table;
var myTabId = _tabId;
return api(BASE + '/' + table).then(function(r) {
if (_tabId !== myTabId) return;
_rows = r.ok ? (Array.isArray(r.data) ? r.data : []) : [];
renderCrudAndLoad();
});
}
window.tab_services = function(tabId) {
var myTabId = tabId;
configCrud('services',
[{field:'title',label:'Название'},{field:'description',label:'Описание',render:function(v){return String(v||'').substring(0,60)}},{field:'is_large',label:'Большая',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}},{field:'sort_order',label:'Порядок'},{field:'is_active',label:'Акт.',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}}],
[{name:'svg_icon',label:'SVG иконка'},{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'items',label:'Пункты списка',type:'json'},{name:'is_large',label:'Большая карточка',type:'number',default:'0'},{name:'sort_order',label:'Порядок',type:'number',default:'0'},{name:'is_active',label:'Активна',type:'number',default:'1'}]
);
};
window.tab_services._name = 'services';
window.tab_schemes = function(tabId) {
var myTabId = tabId;
configCrud('work_schemes',
[{field:'title',label:'Название'},{field:'description',label:'Описание',render:function(v){return String(v||'').substring(0,60)}},{field:'sort_order',label:'Порядок'}],
[{name:'svg_icon',label:'SVG иконка'},{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'items',label:'Пункты списка',type:'json'},{name:'sort_order',label:'Порядок',type:'number',default:'0'},{name:'is_active',label:'Активна',type:'number',default:'1'}]
);
};
window.tab_process = function(tabId) {
var myTabId = tabId;
configCrud('process_steps',
[{field:'step_number',label:'Шаг'},{field:'title',label:'Заголовок'},{field:'description',label:'Описание',render:function(v){return String(v||'').substring(0,60)}}],
[{name:'step_number',label:'Номер шага',type:'number',default:'1'},{name:'title',label:'Заголовок'},{name:'description',label:'Описание',type:'textarea'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}]
);
};
window.tab_equipment = function(tabId) {
var myTabId = tabId;
_table = 'equipment';
_columns = [
{field:'name',label:'Название'},
{field:'brand',label:'Бренд'},
{field:'qty',label:'Кол-во'},
{field:'is_active',label:'Акт.',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}}
];
_fields = [
{name:'name',label:'Название'},{name:'brand',label:'Бренд'},
{name:'image_path',label:'Главное изображение',type:'image'},
{name:'qty',label:'Количество'},{name:'specs',label:'Характеристики',type:'json'},
{name:'sort_order',label:'Порядок',type:'number',default:'0'},
{name:'is_active',label:'Активна',type:'number',default:'1'}
];
_uploadPrefix = 'equipment';
api(BASE + '/equipment').then(function(r) {
if (_tabId !== myTabId) return;
_rows = r.ok ? (Array.isArray(r.data) ? r.data : []) : [];
renderCrudAndLoad();
});
var origEdit = editItem;
editItem = function(id) {
var row = _rows.find(function(r) { return r.id === id; });
if (!row) return;
openModal('Редактировать', function(content) {
content.innerHTML = '<form id="_itemForm">' + buildForm('', _fields, row, true, content) + `
<div style="border-top:1px solid var(--border);padding-top:16px;margin-top:8px">
<h4 style="margin-bottom:12px;color:var(--orange-dark)">Галерея изображений</h4>
<div id="galleryList" style="display:flex;flex-wrap:wrap;gap:10px;margin-bottom:12px"></div>
<div class="form-row">
<div class="form-group2">
<label>Загрузить фото</label>
<input type="file" id="galUpload" multiple accept=".jpg,.jpeg,.png,.webp" style="padding:8px">
</div>
</div>
<button type="button" class="btn btn-sm btn-primary" onclick="winUploadGallery(${id})" style="margin-top:6px">
<i class="fas fa-upload"></i> Загрузить выбранные
</button>
</div>
`;
content.querySelector('#_itemForm').addEventListener('submit', function(e) {
e.preventDefault(); submitForm(this, id);
});
loadGallery(id);
});
};
window.winUploadGallery = async function(eqId) {
var inp = document.getElementById('galUpload');
if (!inp.files.length) return;
var fd = new FormData();
for (var i = 0; i < inp.files.length; i++) fd.append('images', inp.files[i]);
var r = await fetch(BASE + '/equipment/gallery/upload', {
method:'POST', headers:{'Authorization':'Bearer '+token}, body:fd
}).then(function(x) { return x.json(); });
if (Array.isArray(r)) {
for (var j = 0; j < r.length; j++) {
await api(BASE+'/equipment/'+eqId+'/gallery', {
method:'POST',
body:JSON.stringify({image_path:r[j].path, sort_order:j})
});
}
toast('Загружено: ' + r.length + ' фото');
loadGallery(eqId);
}
};
window.loadGallery = async function(eqId) {
var r = await api(BASE+'/equipment/'+eqId+'/gallery');
var items = r.ok ? (Array.isArray(r.data) ? r.data : []) : [];
var el = document.getElementById('galleryList');
if (!el) return;
var h = '';
items.forEach(function(img) {
var src = img.image_path;
if (!src.startsWith('http') && !src.startsWith('./')) {
src = (src.startsWith('equip_') || src.indexOf('/') !== -1) ? ('/data/equipment_images/' + src) : ('./img/' + src);
}
h += '<div style="position:relative;width:100px;height:80px;border:1px solid var(--border);border-radius:6px;overflow:hidden">' +
'<img src="' + src + '" style="width:100%;height:100%;object-fit:cover" onerror="this.src=\'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 80%22><text y=%2250%25%22 x=%2250%25%22 text-anchor=%22middle%22>?</text></svg>\'">' +
'<button class="btn-icon delete" style="position:absolute;top:2px;right:2px;background:var(--danger);color:#fff;border-radius:3px;padding:2px 5px;font-size:11px" onclick="event.stopPropagation();winDelGallery(' + img.id + ')">&times;</button>' +
'</div>';
});
el.innerHTML = h || '<span class="text-muted">Нет фото галереи</span>';
};
window.winDelGallery = async function(imgId) {
if (!confirm('Удалить фото?')) return;
var r = await api(BASE+'/equipment/gallery/'+imgId, {method:'DELETE'});
if (r.ok) { toast('Удалено'); loadGallery(_rows.find(function(x){return true}).id); }
};
};
window.tab_partners = function(tabId) {
var myTabId = tabId;
configCrud('partners',
[{field:'name',label:'Название'},{field:'sort_order',label:'Порядок'},{field:'is_active',label:'Акт.',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}}],
[{name:'name',label:'Название'},{name:'logo_path',label:'Логотип',type:'image'},{name:'sort_order',label:'Порядок',type:'number',default:'0'},{name:'is_active',label:'Активен',type:'number',default:'1'}],
'partners'
);
};
window.tab_hero = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/hero-settings'),
api(BASE + '/hero_stats')
]).then(function(results) {
if (_tabId !== myTabId) return;
var settings = results[0].ok ? results[0].data : {};
var stats = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data : []) : [];
_rows = stats;
_columns = [{field:'id',label:'ID'},{field:'label',label:'Название'},{field:'value',label:'Значение'},{field:'is_dynamic',label:'Динамич.',render:function(v){return v==1?'<span class="badge badge-success">Да</span>':'<span class="badge badge-warning">Нет</span>'}},{field:'sort_order',label:'Порядок'}];
_fields = [{name:'label',label:'Название'},{name:'value',label:'Значение'},{name:'is_dynamic',label:'Динамическое ({years})',type:'number',default:'0'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'hero_stats';
_uploadPrefix = 'hero';
var keys = ['hero_bg','badge_text','h1_text','subtitle','phone_800','phone_ekb','email','address','button1_text','button1_link','button2_text','button2_link'];
document.getElementById('mainContent').innerHTML = h() +
'<button class="btn btn-primary" style="margin-bottom:8px" onclick="winEditHeroSettings()"><i class="fas fa-edit"></i> Настройки Hero</button>' +
'<button class="btn btn-primary" style="margin-left:8px;margin-bottom:8px" onclick="createItem()"><i class="fas fa-plus"></i> Добавить статистику</button>' +
'<div class="settings-grid" style="margin-bottom:16px"><div class="settings-card"><h3>Текущие настройки</h3>' +
keys.map(function(k) { return '<p style="margin-bottom:3px"><strong>' + k + ':</strong> <span class="text-muted">' + esc(String(settings[k]||'').substring(0,60)) + '</span></p>'; }).join('') +
'</div></div>' + renderTable();
setupTableHandlers();
window._heroSettings = settings;
window.winEditHeroSettings = function() {
var s = window._heroSettings;
openModal('Hero-настройки', function(content) {
var html = '<form id="_heroForm">';
keys.forEach(function(k) {
html += '<div class="form-group2"><label>' + k + '</label><input name="' + k + '" value="' + esc(s[k]||'') + '"></div>';
});
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Сохранить</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_heroForm').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k){body[k]=v;});
api(BASE+'/hero-settings', { method:'PUT', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Сохранено');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
});
};
window.tab_about = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/about-settings'),
api(BASE + '/about_features')
]).then(function(results) {
if (_tabId !== myTabId) return;
var settings = results[0].ok ? results[0].data : {};
var features = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data : []) : [];
_rows = features;
_columns = [{field:'title',label:'Заголовок'},{field:'description',label:'Описание'},{field:'sort_order',label:'Порядок'}];
_fields = [{name:'svg_icon',label:'SVG иконка'},{name:'title',label:'Заголовок'},{name:'description',label:'Описание'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'about_features';
_uploadPrefix = 'about';
var aboutKeys = ['section_subtitle','main_heading','about_image','paragraph_1','paragraph_2','paragraph_3'];
document.getElementById('mainContent').innerHTML = h() +
'<button class="btn btn-primary" style="margin-bottom:8px" onclick="winEditAboutSettings()"><i class="fas fa-edit"></i> Настройки раздела</button>' +
'<button class="btn btn-primary" style="margin-left:8px;margin-bottom:8px" onclick="createItem()"><i class="fas fa-plus"></i> Добавить преимущество</button>' +
renderTable();
setupTableHandlers();
window._aboutSettings = settings;
window.winEditAboutSettings = function() {
var s = window._aboutSettings;
openModal('Настройки "О компании"', function(content) {
var html = '<form id="_aboutForm">';
aboutKeys.forEach(function(k) {
html += '<div class="form-group2"><label>' + k + '</label><textarea name="' + k + '">' + esc(s[k]||'') + '</textarea></div>';
});
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Сохранить</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_aboutForm').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k){body[k]=v;});
api(BASE+'/about-settings', { method:'PUT', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Сохранено');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
});
};
window.tab_geography = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/geo-settings'),
api(BASE + '/geo_regions')
]).then(function(results) {
if (_tabId !== myTabId) return;
var settings = results[0].ok ? results[0].data : {};
var regions = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data : []) : [];
_rows = regions;
_columns = [{field:'name',label:'Регион'},{field:'sort_order',label:'Порядок'}];
_fields = [{name:'name',label:'Название'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'geo_regions';
_uploadPrefix = 'geography';
var geoKeys = ['section_subtitle','difficult_title','difficult_desc','map_image'];
document.getElementById('mainContent').innerHTML = h() +
'<button class="btn btn-primary" style="margin-bottom:8px" onclick="winEditGeoSettings()"><i class="fas fa-edit"></i> Гео-настройки</button>' +
'<button class="btn btn-primary" style="margin-left:8px;margin-bottom:8px" onclick="createItem()"><i class="fas fa-plus"></i> Добавить регион</button>' +
renderTable();
setupTableHandlers();
window._geoSettings = settings;
window.winEditGeoSettings = function() {
var s = window._geoSettings;
openModal('Гео-настройки', function(content) {
var html = '<form id="_geoForm">';
geoKeys.forEach(function(k) {
html += '<div class="form-group2"><label>' + k + '</label><textarea name="' + k + '">' + esc(s[k]||'') + '</textarea></div>';
});
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Сохранить</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_geoForm').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k){body[k]=v;});
api(BASE+'/geo-settings', { method:'PUT', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Сохранено');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
});
};
window.tab_licenses = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/licenses'),
api(BASE + '/license_gallery')
]).then(function(results) {
if (_tabId !== myTabId) return;
var cards = results[0].ok ? (Array.isArray(results[0].data) ? results[0].data : []) : [];
var gallery = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data : []) : [];
var html = h();
html += '<div style="margin-bottom:8px"><strong style="color:var(--orange)">Лицензии (карточки)</strong></div>';
_rows = cards;
_columns = [{field:'title',label:'Название'},{field:'description',label:'Описание',render:function(v){return String(v||'').substring(0,50)}}];
_fields = [{name:'svg_icon',label:'SVG иконка'},{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'licenses';
_uploadPrefix = 'licenses';
html += '<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="createItem()"><i class="fas fa-plus"></i> Добавить</button>';
html += renderTable();
html += '<div style="margin-top:20px"><strong style="color:var(--orange)">Галерея лицензий</strong></div>';
html += '<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="winSwLicGal()"><i class="fas fa-plus"></i> Добавить</button>';
html += renderGalleryTable(gallery);
document.getElementById('mainContent').innerHTML = html;
setupTableHandlers();
window.winSwLicGal = function() {
_rows = gallery;
_columns = [{field:'title',label:'Название'},{field:'description',label:'Описание'}];
_fields = [{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'image_path',label:'Изображение',type:'image'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'license_gallery';
_uploadPrefix = 'licenses';
createItem();
};
});
};
function renderGalleryTable(rows) {
if (!rows || rows.length === 0) return '<p class="text-muted">Нет данных</p>';
var html = '<div class="table-wrap"><table><thead><tr><th>Название</th><th>Описание</th><th>Действия</th></tr></thead><tbody>';
rows.forEach(function(row) {
html += '<tr><td>' + esc(row.title) + '</td><td>' + esc(String(row.description||'').substring(0,50)) + '</td><td class="actions">';
html += '<button class="btn-icon edit" data-action="edit" data-id="' + row.id + '"><i class="fas fa-edit"></i></button>';
html += '<button class="btn-icon delete" data-action="delete" data-id="' + row.id + '"><i class="fas fa-trash"></i></button>';
html += '</td></tr>';
});
html += '</tbody></table></div>';
return html;
}
window.tab_activities = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/public_activities'),
api(BASE + '/activity_gallery')
]).then(function(results) {
if (_tabId !== myTabId) return;
var cards = results[0].ok ? (Array.isArray(results[0].data) ? results[0].data : []) : [];
var gallery = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data : []) : [];
_rows = cards;
_columns = [{field:'title',label:'Название'},{field:'description',label:'Описание',render:function(v){return String(v||'').substring(0,50)}}];
_fields = [{name:'svg_icon',label:'SVG иконка'},{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'public_activities';
_uploadPrefix = 'activities';
document.getElementById('mainContent').innerHTML = h() +
'<div style="margin-bottom:8px"><strong style="color:var(--orange)">Карточки деятельности</strong></div>' +
'<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="createItem()" id="btnActCards"><i class="fas fa-plus"></i> Добавить</button>' +
renderTable() +
'<div style="margin-top:20px"><strong style="color:var(--orange)">Благодарственные письма</strong></div>' +
'<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="winSwActGal()"><i class="fas fa-plus"></i> Добавить</button>' +
renderGalleryTable(gallery);
setupTableHandlers();
window.winSwActGal = function() {
_rows = gallery;
_columns = [{field:'title',label:'Название'},{field:'description',label:'Описание'}];
_fields = [{name:'title',label:'Название'},{name:'description',label:'Описание',type:'textarea'},{name:'image_path',label:'Изображение',type:'image'},{name:'sort_order',label:'Порядок',type:'number',default:'0'}];
_table = 'activity_gallery';
_uploadPrefix = 'activities';
createItem();
};
});
};
window.tab_settings = function(tabId) {
var myTabId = tabId;
document.getElementById('mainContent').innerHTML = h() + '<div style="text-align:center;padding:30px"><i class="fas fa-spinner fa-spin"></i></div>';
api(BASE + '/settings').then(function(r) {
var s = r.ok ? r.data : {};
var contactKeys = ['contact_phone_800','contact_phone_ekb','contact_email','contact_address','contact_work_hours','contact_map_image','footer_text'];
var formKeys = ['form_title','form_subtitle','section_services_subtitle','section_schemes_subtitle','section_process_subtitle','section_equipment_subtitle','section_licenses_subtitle','section_activity_subtitle','section_partners_subtitle','section_contacts_subtitle'];
document.getElementById('mainContent').innerHTML = h() +
'<div class="settings-grid">' +
'<div class="settings-card"><h3>Логотип сайта</h3>' +
'<div id="logoPreview" style="margin-bottom:10px">' +
(s.logo_path ? '<img src="' + s.logo_path + '" class="preview-img" style="max-height:80px">' : '<span class="text-muted">Логотип не загружен</span>') +
'</div>' +
'<input type="file" id="logoFileInput" accept=".jpg,.jpeg,.png,.webp,.svg,.gif,.bmp" style="margin-bottom:8px;width:100%">' +
'<button class="btn btn-sm btn-primary" onclick="winUploadLogo()"><i class="fas fa-upload"></i> Загрузить логотип</button>' +
'<p style="font-size:.75rem;color:var(--text-gray);margin-top:3px">При загрузке автоматически создаются favicon и apple-touch-icon</p>' +
'</div>' +
'<div class="settings-card"><h3>Контакты</h3>' +
contactKeys.map(function(k) { return '<p style="margin-bottom:4px"><strong>' + k + ':</strong> <span class="text-muted">' + esc(String(s[k]||'').substring(0,80)) + '</span></p>'; }).join('') +
'<button class="btn btn-sm btn-primary" style="margin-top:8px" onclick="winEditSettings(\'contacts\')"><i class="fas fa-edit"></i> Редактировать</button>' +
'</div>' +
'<div class="settings-card"><h3>Форма и подзаголовки</h3>' +
formKeys.map(function(k) { return '<p style="margin-bottom:4px"><strong>' + k.replace('section_','').substring(0,20) + ':</strong> <span class="text-muted">' + esc(String(s[k]||'').substring(0,60)) + '</span></p>'; }).join('') +
'<button class="btn btn-sm btn-primary" style="margin-top:8px" onclick="winEditSettings(\'form\')"><i class="fas fa-edit"></i> Редактировать</button>' +
'</div>' +
'</div>';
window._settings = s;
window.winUploadLogo = async function() {
var inp = document.getElementById('logoFileInput');
if (!inp.files.length) { toast('Выберите файл','error'); return; }
var fd = new FormData();
fd.append('logo', inp.files[0]);
try {
var r = await fetch(BASE + '/settings/logo', {
method: 'POST', headers: {'Authorization':'Bearer ' + token}, body: fd
}).then(function(x) { return x.json(); });
if (r.path) {
toast('Логотип обновлён! Favicon сгенерированы');
loadCurrentTab();
} else {
toast(r.error || 'Ошибка загрузки','error');
}
} catch(e) {
toast('Ошибка загрузки','error');
}
};
window.winEditSettings = function(mode) {
var s = window._settings;
var keys = mode === 'contacts' ? contactKeys : formKeys;
openModal('Редактировать', function(content) {
var html = '<form id="_setForm">';
keys.forEach(function(k) {
html += '<div class="form-group2"><label>' + k + '</label><input name="' + k + '" value="' + esc(s[k]||'') + '"></div>';
});
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Сохранить</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_setForm').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k){body[k]=v;});
api(BASE+'/settings', { method:'PUT', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Сохранено');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
});
};
window.tab_leads = function(tabId) {
var myTabId = tabId;
var page = 1;
function renderLeads(data) {
_rows = data.data || [];
_columns = [{field:'id',label:'ID'},{field:'name',label:'Имя'},{field:'phone',label:'Телефон'},{field:'message',label:'Сообщение',render:function(v){return String(v||'').substring(0,60)}},{field:'created_at',label:'Дата'}];
_table = 'leads';
_fields = [];
_uploadPrefix = 'leads';
document.getElementById('mainContent').innerHTML = h() +
'<div class="search-bar"><input id="leadSearch" placeholder="Поиск по заявкам..." oninput="winSearchLeads()"></div>' +
((_rows && _rows.length > 0) ? renderTable() : '<p class="text-muted">Нет заявок</p>') +
'<div class="pagination"><button ' + (data.page<=1?'disabled':'') + ' onclick="winGoPage(' + (data.page-1) + ')">&#8592;</button><span>' + data.page + ' / ' + data.totalPages + '</span><button ' + (data.page>=data.totalPages?'disabled':'') + ' onclick="winGoPage(' + (data.page+1) + ')">&#8594;</button></div>';
setupTableHandlers();
}
window.winSearchLeads = function() {
page = 1;
var q = document.getElementById('leadSearch').value;
api(BASE + '/leads?page=1&search=' + encodeURIComponent(q)).then(function(r) {
if (r.ok) renderLeads(r.data);
});
};
window.winGoPage = function(p) {
page = p;
var q = document.getElementById('leadSearch') ? document.getElementById('leadSearch').value : '';
api(BASE + '/leads?page=' + p + '&search=' + encodeURIComponent(q)).then(function(r) {
if (r.ok) renderLeads(r.data);
});
};
api(BASE + '/leads?page=1').then(function(r) {
renderLeads(r.ok ? r.data : { data: [], total: 0, page: 1, totalPages: 1 });
});
};
window.tab_users = function(tabId) {
var myTabId = tabId;
document.getElementById('mainContent').innerHTML = h() + '<div style="text-align:center;padding:30px"><i class="fas fa-spinner fa-spin"></i></div>';
api(BASE + '/users').then(function(r) {
var rows = r.ok ? (Array.isArray(r.data) ? r.data : []) : [];
_rows = rows;
_columns = [{field:'id',label:'ID'},{field:'login',label:'Логин'},{field:'full_name',label:'ФИО'},{field:'role',label:'Роль',render:function(v){return '<span class="badge badge-success">'+v+'</span>'}},{field:'created_at',label:'Создан'}];
_table = 'users';
_uploadPrefix = 'users';
document.getElementById('mainContent').innerHTML = h() +
'<button class="btn btn-primary" style="margin-bottom:16px" onclick="winCreateUser()"><i class="fas fa-plus"></i> Добавить</button>' +
renderTable();
setupTableHandlers();
window.winCreateUser = function() {
openModal('Создать пользователя', function(content) {
var html = '<form id="_userForm"><div class="form-group2"><label>Логин</label><input name="login" required></div><div class="form-group2"><label>Пароль</label><input name="password" type="password" required></div><div class="form-group2"><label>ФИО</label><input name="full_name"></div><div class="form-group2"><label>Роль</label><select name="role"><option value="admin">admin</option></select></div><div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Создать</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_userForm').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k){body[k]=v;});
api(BASE+'/users', { method:'POST', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Создан');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
});
var origEdit = editItem;
editItem = function(id) {
var row = _rows.find(function(r) { return r.id === id; });
if (!row) return;
openModal('Редактировать', function(content) {
var html = '<form id="_userEdit">';
html += '<div class="form-group2"><label>Логин</label><input name="login" value="' + esc(row.login) + '" required></div>';
html += '<div class="form-group2"><label>Новый пароль (пусто = не менять)</label><input name="password" type="password"></div>';
html += '<div class="form-group2"><label>ФИО</label><input name="full_name" value="' + esc(row.full_name||'') + '"></div>';
html += '<div class="form-group2"><label>Роль</label><select name="role"><option value="admin" ' + (row.role==='admin'?'selected':'') + '>admin</option></select></div>';
html += '<div class="modal-actions"><button type="button" class="btn" onclick="closeModal()">Отмена</button><button type="submit" class="btn btn-primary">Сохранить</button></div></form>';
content.insertAdjacentHTML('beforeend', html);
content.querySelector('#_userEdit').addEventListener('submit', function(e) {
e.preventDefault();
var body = {};
new FormData(this).forEach(function(v,k) { if(v) body[k]=v; });
api(BASE+'/users/'+id, { method:'PUT', body:JSON.stringify(body) }).then(function(r) {
if(r.ok){toast('Сохранено');closeModal();loadCurrentTab();}else toast(r.data.error,'error');
});
});
});
};
};
window.tab_dashboard = function(tabId) {
var myTabId = tabId;
Promise.all([
api(BASE + '/leads?page=1'),
api(BASE + '/users')
]).then(function(results) {
if (_tabId !== myTabId) return;
var totalLeads = results[0].ok ? (results[0].data.total || 0) : 0;
var userCount = results[1].ok ? (Array.isArray(results[1].data) ? results[1].data.length : 0) : 0;
document.getElementById('mainContent').innerHTML = h() +
'<div class="stats-grid">' +
'<div class="stat-card"><i class="fas fa-envelope"></i><div class="num">' + totalLeads + '</div><div class="lbl">Всего заявок</div></div>' +
'<div class="stat-card"><i class="fas fa-users"></i><div class="num">' + userCount + '</div><div class="lbl">Пользователей</div></div>' +
'<div class="stat-card"><i class="fas fa-database"></i><div class="num">14</div><div class="lbl">Разделов сайта</div></div>' +
'</div>' +
'<p class="text-muted">Выберите раздел в боковом меню для редактирования.</p>';
});
};
function enterPanel() {
document.getElementById('login-page').style.display = 'none';
document.getElementById('admin-panel').style.display = 'flex';
document.querySelectorAll('.sidebar-nav a[data-tab]').forEach(function(a) {
a.addEventListener('click', function(e) {
e.preventDefault();
document.querySelectorAll('.sidebar-nav a[data-tab]').forEach(function(x) { x.classList.remove('active'); });
this.classList.add('active');
currentTab = this.dataset.tab;
loadCurrentTab();
});
});
document.getElementById('mainContent').innerHTML = '<div style="text-align:center;padding:60px"><i class="fas fa-spinner fa-spin" style="font-size:28px;color:var(--orange)"></i></div>';
loadCurrentTab();
}
function logout() {
localStorage.removeItem('adminToken'); token = null; user = null;
document.getElementById('login-page').style.display = 'flex';
document.getElementById('admin-panel').style.display = 'none';
_rows = []; _table = ''; _columns = []; _fields = [];
}
})();
</script>
</body>
</html>