1016 lines
57 KiB
HTML
1016 lines
57 KiB
HTML
<!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()">×</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,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
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))">×</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))">×</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))">×</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))">×</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 + ')">×</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) + ')">←</button><span>' + data.page + ' / ' + data.totalPages + '</span><button ' + (data.page>=data.totalPages?'disabled':'') + ' onclick="winGoPage(' + (data.page+1) + ')">→</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>
|