742 lines
35 KiB
HTML
742 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Система учёта клиентов</title>
|
||
<style>
|
||
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--green:#10B981;--red:#EF4444;--blue:#3B82F6}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
|
||
.container{max-width:1400px;margin:0 auto;padding:24px}
|
||
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-wrap:wrap;gap:12px}
|
||
.header h1{font-size:24px;font-weight:700}
|
||
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
|
||
.tab{padding:8px 16px;border:1px solid var(--gray-100);border-radius:8px;cursor:pointer;background:var(--white);font-weight:500}
|
||
.tab.active{background:var(--ink);color:var(--white);border-color:var(--ink)}
|
||
.btn{padding:10px 20px;border-radius:8px;border:none;cursor:pointer;font-weight:600;font-size:14px}
|
||
.btn-primary{background:var(--cyan);color:var(--ink)}
|
||
.btn-success{background:var(--green);color:var(--white)}
|
||
.btn-danger{background:var(--red);color:var(--white)}
|
||
.btn-secondary{background:var(--gray-100);color:var(--ink)}
|
||
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
|
||
.table-container{overflow-x:auto;background:var(--white);border-radius:12px;border:1px solid var(--gray-100)}
|
||
table{width:100%;border-collapse:collapse;min-width:1200px}
|
||
th,td{padding:12px;text-align:left;border-bottom:1px solid var(--gray-100);white-space:nowrap}
|
||
th{background:var(--gray-100);font-weight:600;position:sticky;top:0}
|
||
tr:hover{background:var(--cyan-50)}
|
||
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;overflow-y:auto}
|
||
.modal.open{display:block}
|
||
.modal-content{background:var(--white);margin:40px auto;max-width:800px;border-radius:12px;padding:24px;max-height:90vh;overflow-y:auto}
|
||
.form-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:16px;margin-bottom:16px}
|
||
.form-group{display:flex;flex-direction:column;gap:4px}
|
||
.form-group label{font-weight:500;font-size:13px;color:var(--gray-500)}
|
||
.form-group input,.form-group select,.form-group textarea{padding:10px;border:1px solid var(--gray-100);border-radius:8px;font-size:14px}
|
||
.form-group textarea{resize:vertical;min-height:80px}
|
||
.actions{display:flex;gap:8px}
|
||
.edit-cell{cursor:pointer;color:var(--blue)}
|
||
.edit-cell:hover{text-decoration:underline}
|
||
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
|
||
.stat-card{background:var(--gray-100);padding:20px;border-radius:12px}
|
||
.stat-card h3{font-size:14px;color:var(--gray-500);margin-bottom:8px}
|
||
.stat-card .value{font-size:28px;font-weight:700;color:var(--ink)}
|
||
.filter-bar{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px;align-items:center}
|
||
.filter-bar input,.filter-bar select{padding:8px 12px;border:1px solid var(--gray-100);border-radius:8px;font-size:14px}
|
||
.login-form{max-width:400px;margin:60px auto}
|
||
.user-info{display:flex;align-items:center;gap:12px}
|
||
.avatar{width:36px;height:36px;border-radius:50%;background:var(--cyan);display:flex;align-items:center;justify-content:center;font-weight:700}
|
||
.permission-badge{font-size:11px;padding:4px 8px;background:var(--cyan-50);border-radius:4px;color:var(--ink)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div id="loginSection">
|
||
<div id="loginForm" class="login-form" style="max-width:400px">
|
||
<h1 style="margin-bottom:24px">Вход в систему</h1>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>ФИО</label>
|
||
<input type="text" id="loginFullName" placeholder="Введите ваше ФИО">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Пароль</label>
|
||
<input type="password" id="loginPassword" placeholder="Введите пароль">
|
||
</div>
|
||
<button class="btn btn-primary" onclick="doLogin()" style="width:100%">Войти</button>
|
||
<p style="margin-top:24px;text-align:center;color:var(--gray-500)">Нет учётной записи?</p>
|
||
<button class="btn btn-secondary" onclick="showRegistration()" style="width:100%">Зарегистрироваться</button>
|
||
</div>
|
||
|
||
<div id="registrationForm" class="login-form" style="max-width:500px;display:none">
|
||
<h1 style="margin-bottom:24px">Регистрация</h1>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>ФИО *</label>
|
||
<input type="text" id="regFullName" placeholder="Иванов Иван Иванович">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Номер телефона *</label>
|
||
<input type="tel" id="regPhone" placeholder="+7 777 123 45 67">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Филиал *</label>
|
||
<input type="text" id="regBranch" placeholder="Например: Алматы филиал">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Отдел *</label>
|
||
<input type="text" id="regDepartment" placeholder="Например: Отдел продаж">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Должность *</label>
|
||
<input type="text" id="regPosition" placeholder="Например: Менеджер">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Пароль *</label>
|
||
<input type="password" id="regPassword" placeholder="Придумайте пароль">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Подтверждение пароля *</label>
|
||
<input type="password" id="regPasswordConfirm" placeholder="Повторите пароль">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:16px">
|
||
<label>Запрашиваемая роль *</label>
|
||
<select id="regRole">
|
||
<option value="view">Просмотр (только чтение)</option>
|
||
<option value="editor">Редактор (добавление и редактирование)</option>
|
||
<option value="curator">Куратор (полный доступ)</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="doRegister()" style="width:100%">Зарегистрироваться</button>
|
||
<button class="btn btn-secondary" onclick="showLogin()" style="width:100%;margin-top:12px">Вернуться ко входу</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="mainSection" style="display:none">
|
||
<div class="header">
|
||
<h1>📊 Система учёта клиентов</h1>
|
||
<div class="user-info" style="flex-wrap:wrap;gap:8px">
|
||
<div class="avatar" id="userAvatar"></div>
|
||
<span id="userName"></span>
|
||
<span class="permission-badge" id="userPermissions"></span>
|
||
<span class="permission-badge" id="userInfoDetails"></span>
|
||
<button class="btn btn-secondary" onclick="logout()">Выйти</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tabs" id="sheetTabs"></div>
|
||
|
||
<div class="toolbar">
|
||
<button class="btn btn-primary" id="addRecordBtn" onclick="openModal()">+ Добавить запись</button>
|
||
<button class="btn btn-success" onclick="exportExcel()">📥 Excel</button>
|
||
<button class="btn btn-primary" onclick="openStatsModal()">📈 Статистика</button>
|
||
<button class="btn btn-primary" onclick="openPermissionsModal()">🔐 Доступы</button>
|
||
<button class="btn btn-primary" onclick="openUsersModal()">👥 Пользователи</button>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<input type="text" id="searchInput" placeholder="🔍 Поиск..." oninput="renderTable()" style="flex:1;min-width:200px">
|
||
<select id="filterRegion" onchange="renderTable()">
|
||
<option value="">Все области</option>
|
||
</select>
|
||
<select id="filterStatus" onchange="renderTable()">
|
||
<option value="">Все статусы</option>
|
||
<option value="Подключен">Подключен</option>
|
||
<option value="В работе">В работе</option>
|
||
<option value="Планирование">Планирование</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>№</th>
|
||
<th>Сегмент</th>
|
||
<th>Область</th>
|
||
<th>Район</th>
|
||
<th>Нас. пункт</th>
|
||
<th>Адрес</th>
|
||
<th>Клиент</th>
|
||
<th>Деятельность</th>
|
||
<th>Тех. возможность</th>
|
||
<th>Статус строительства</th>
|
||
<th>Начало</th>
|
||
<th>Завершение</th>
|
||
<th>Примечание</th>
|
||
<th>№ устр-ва</th>
|
||
<th>Статус</th>
|
||
<th>Дата подключ.</th>
|
||
<th>Тариф</th>
|
||
<th>ARPU</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="recordModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2 style="margin-bottom:20px" id="modalTitle">Новая запись</h2>
|
||
<div class="form-grid">
|
||
<div class="form-group"><label>№</label><input type="text" id="recNum"></div>
|
||
<div class="form-group"><label>Сегмент</label><input type="text" id="recSegment"></div>
|
||
<div class="form-group"><label>Область</label><input type="text" id="recRegion"></div>
|
||
<div class="form-group"><label>Район</label><input type="text" id="recDistrict"></div>
|
||
<div class="form-group"><label>Населенный пункт</label><input type="text" id="recSettlement"></div>
|
||
<div class="form-group"><label>Адрес</label><input type="text" id="recAddress"></div>
|
||
<div class="form-group"><label>Наименование клиента</label><input type="text" id="recClientName"></div>
|
||
<div class="form-group"><label>Вид деятельности</label><input type="text" id="recActivity"></div>
|
||
<div class="form-group"><label>Тех. возможность</label><select id="recTechPossibility"><option value="">Не выбрано</option><option value="Есть">Есть</option><option value="Нет">Нет</option><option value="Требует уточнения">Требует уточнения</option></select></div>
|
||
<div class="form-group"><label>Статус строительства</label><select id="recConstructionStatus"><option value="">Не выбрано</option><option value="Не начинается">Не начинается</option><option value="В работе">В работе</option><option value="Завершено">Завершено</option></select></div>
|
||
<div class="form-group"><label>Начало строительства</label><input type="date" id="recConstructionStart"></div>
|
||
<div class="form-group"><label>Сроки завершения</label><input type="date" id="recConstructionEnd"></div>
|
||
<div class="form-group" style="grid-column:1/-1"><label>Примечание</label><textarea id="recNote"></textarea></div>
|
||
<div class="form-group"><label>№ устройства</label><input type="text" id="recDeviceNum"></div>
|
||
<div class="form-group"><label>Статус подключения</label><select id="recConnectionStatus"><option value="">Не выбрано</option><option value="Подключен">Подключен</option><option value="В работе">В работе</option><option value="Планирование">Планирование</option><option value="Отказ">Отказ</option></select></div>
|
||
<div class="form-group"><label>Дата подключения</label><input type="date" id="recConnectionDate"></div>
|
||
<div class="form-group"><label>Тарифный план</label><input type="text" id="recTariff"></div>
|
||
<div class="form-group"><label>ARPU (тыс. тг)</label><input type="number" step="0.01" id="recArpu"></div>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" onclick="saveRecord()">Сохранить</button>
|
||
<button class="btn btn-secondary" onclick="closeModal()">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="statsModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2 style="margin-bottom:20px">📈 Статистика</h2>
|
||
<div class="filter-bar">
|
||
<div class="form-group"><label>С даты</label><input type="date" id="statsStartDate"></div>
|
||
<div class="form-group"><label>По дату</label><input type="date" id="statsEndDate"></div>
|
||
<div class="form-group"><label>Группировать по</label>
|
||
<select id="statsGroupBy">
|
||
<option value="region">Область</option>
|
||
<option value="segment">Сегмент</option>
|
||
<option value="connectionStatus">Статус подключения</option>
|
||
<option value="constructionStatus">Статус строительства</option>
|
||
<option value="activity">Вид деятельности</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary" onclick="loadStats()">Показать</button>
|
||
</div>
|
||
<div class="stats-grid" id="statsGrid"></div>
|
||
<div class="actions">
|
||
<button class="btn btn-secondary" onclick="document.getElementById('statsModal').classList.remove('open')">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="permissionsModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2 style="margin-bottom:20px">🔐 Настройка доступов</h2>
|
||
<div class="form-grid" style="margin-bottom:16px">
|
||
<div class="form-group"><label>Пользователь</label><select id="permUser"></select></div>
|
||
<div class="form-group"><label>Лист</label><select id="permSheet"><option value="all">Все листы</option></select></div>
|
||
<div class="form-group"><label>Права</label>
|
||
<select id="permLevel">
|
||
<option value="view">Только просмотр</option>
|
||
<option value="edit">Редактирование</option>
|
||
<option value="full">Полный доступ</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions" style="margin-bottom:16px">
|
||
<button class="btn btn-primary" onclick="addPermission()">Добавить доступ</button>
|
||
</div>
|
||
<div id="permissionsList"></div>
|
||
<div class="actions" style="margin-top:16px">
|
||
<button class="btn btn-secondary" onclick="document.getElementById('permissionsModal').classList.remove('open')">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="usersModal" class="modal">
|
||
<div class="modal-content">
|
||
<h2 style="margin-bottom:20px">👥 Пользователи</h2>
|
||
<div class="form-grid" style="margin-bottom:16px">
|
||
<div class="form-group"><label>Имя</label><input type="text" id="newUserName"></div>
|
||
<div class="form-group"><label>Роль</label>
|
||
<select id="newUserRole">
|
||
<option value="user">Пользователь</option>
|
||
<option value="admin">Администратор</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions" style="margin-bottom:16px">
|
||
<button class="btn btn-primary" onclick="addUser()">Добавить</button>
|
||
</div>
|
||
<div id="usersList"></div>
|
||
<div class="actions" style="margin-top:16px">
|
||
<button class="btn btn-secondary" onclick="document.getElementById('usersModal').classList.remove('open')">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let allRecords = [];
|
||
let currentSheet = "all";
|
||
let editingId = null;
|
||
|
||
const FIELDS = ["num","segment","region","district","settlement","address","clientName","activity","techPossibility","constructionStatus","constructionStart","constructionEnd","note","deviceNum","connectionStatus","connectionDate","tariff","arpu"];
|
||
|
||
function showLogin() {
|
||
document.getElementById("loginForm").style.display = "block";
|
||
document.getElementById("registrationForm").style.display = "none";
|
||
}
|
||
|
||
function showRegistration() {
|
||
document.getElementById("loginForm").style.display = "none";
|
||
document.getElementById("registrationForm").style.display = "block";
|
||
}
|
||
|
||
async function doLogin() {
|
||
const fullName = document.getElementById("loginFullName").value.trim();
|
||
const password = document.getElementById("loginPassword").value;
|
||
|
||
if (!fullName) return alert("Введите ФИО");
|
||
if (!password) return alert("Введите пароль");
|
||
|
||
const res = await fetch("/api/users");
|
||
const data = await res.json();
|
||
let user = data.users.find(u => u.fullName === fullName);
|
||
|
||
if (!user) {
|
||
const recover = confirm(`Пользователь "${fullName}" не найден.\n\nВосстановить доступ через Telegram?`);
|
||
if (recover) {
|
||
window.open("https://t.me/vibe42_support", "_blank");
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (user.password !== password) {
|
||
const recover = confirm("Неверный пароль.\n\nВосстановить доступ через Telegram?");
|
||
if (recover) {
|
||
window.open("https://t.me/vibe42_support", "_blank");
|
||
}
|
||
return;
|
||
}
|
||
|
||
currentUser = user;
|
||
document.getElementById("loginSection").style.display = "none";
|
||
document.getElementById("mainSection").style.display = "block";
|
||
document.getElementById("userAvatar").textContent = fullName[0].toUpperCase();
|
||
document.getElementById("userName").textContent = fullName;
|
||
const roleText = user.approvedRole === "curator" ? "Куратор" : user.approvedRole === "editor" ? "Редактор" : "Просмотр";
|
||
const requestedText = user.requestedRole !== user.approvedRole ? ` (запрошено: ${user.requestedRole === 'curator' ? 'Куратор' : user.requestedRole === 'editor' ? 'Редактор' : 'Просмотр'})` : '';
|
||
document.getElementById("userPermissions").textContent = `${roleText}${requestedText}`;
|
||
document.getElementById("userInfoDetails").textContent = `${user.branch || '-'} | ${user.department || '-'}`;
|
||
|
||
initApp();
|
||
}
|
||
|
||
async function doRegister() {
|
||
const fullName = document.getElementById("regFullName").value.trim();
|
||
const phone = document.getElementById("regPhone").value.trim();
|
||
const branch = document.getElementById("regBranch").value.trim();
|
||
const department = document.getElementById("regDepartment").value.trim();
|
||
const position = document.getElementById("regPosition").value.trim();
|
||
const password = document.getElementById("regPassword").value;
|
||
const passwordConfirm = document.getElementById("regPasswordConfirm").value;
|
||
const requestedRole = document.getElementById("regRole").value;
|
||
|
||
if (!fullName) return alert("Введите ФИО");
|
||
if (!phone) return alert("Введите номер телефона");
|
||
if (!branch) return alert("Введите филиал");
|
||
if (!department) return alert("Введите отдел");
|
||
if (!position) return alert("Введите должность");
|
||
if (!password) return alert("Введите пароль");
|
||
if (password !== passwordConfirm) return alert("Пароли не совпадают");
|
||
|
||
const res = await fetch("/api/users");
|
||
const data = await res.json();
|
||
let user = data.users.find(u => u.fullName === fullName);
|
||
|
||
if (user) {
|
||
alert("Пользователь с таким ФИО уже существует. Войдите.");
|
||
showLogin();
|
||
return;
|
||
}
|
||
|
||
user = {
|
||
fullName: fullName,
|
||
phone: phone,
|
||
branch: branch,
|
||
department: department,
|
||
position: position,
|
||
password: password,
|
||
requestedRole: requestedRole,
|
||
role: "user",
|
||
approvedRole: "view"
|
||
};
|
||
const result = await fetch("/api/users", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(user) });
|
||
const userData = await result.json();
|
||
user.id = userData.ok ? Date.now() : user.id;
|
||
|
||
const allPerms = await fetch("/api/permissions").then(r => r.json());
|
||
const hasDefaultPerm = allPerms.permissions.some(p => p.userName === fullName);
|
||
if (!hasDefaultPerm) {
|
||
await fetch("/api/permissions", {
|
||
method: "POST",
|
||
headers: {"Content-Type":"application/json"},
|
||
body: JSON.stringify({ userId: fullName, userName: fullName, sheetName: "", level: "view" })
|
||
});
|
||
}
|
||
|
||
alert("✅ Регистрация успешна!\n\nТеперь вы можете войти в систему.");
|
||
showLogin();
|
||
}
|
||
|
||
function logout() {
|
||
currentUser = null;
|
||
document.getElementById("loginSection").style.display = "block";
|
||
document.getElementById("mainSection").style.display = "none";
|
||
}
|
||
|
||
async function initApp() {
|
||
await loadRecords();
|
||
await loadUserPermissions();
|
||
loadTabs();
|
||
renderTable();
|
||
updateUIByPermission();
|
||
}
|
||
|
||
function updateUIByPermission() {
|
||
const canEdit = currentUser && (currentUser.role === "admin" || currentUser.approvedRole === "editor" || currentUser.approvedRole === "curator" || canEditRecord({}));
|
||
const addBtn = document.getElementById("addRecordBtn");
|
||
if (addBtn) {
|
||
addBtn.style.display = canEdit ? "inline-block" : "none";
|
||
}
|
||
}
|
||
|
||
async function loadUserPermissions() {
|
||
if (!currentUser) return;
|
||
const res = await fetch("/api/permissions");
|
||
const data = await res.json();
|
||
window.userPermissions = data.permissions.filter(p => p.userName === currentUser.fullName || p.userId === currentUser.fullName);
|
||
}
|
||
|
||
async function loadRecords() {
|
||
const res = await fetch("/api/records");
|
||
const data = await res.json();
|
||
allRecords = data.records || [];
|
||
|
||
const regions = [...new Set(allRecords.map(r => r.region).filter(Boolean))];
|
||
const filterRegion = document.getElementById("filterRegion");
|
||
filterRegion.innerHTML = '<option value="">Все области</option>' + regions.map(r => `<option value="${r}">${r}</option>`).join("");
|
||
|
||
const sheets = ["all", "Алматы", "Астана", "Шымкент", "Алматинская", "Жамбылская", "Кызылординская", "Мангистауская", "Павлодарская", "Восточно-Казахстанская", "Северо-Казахстанская", "Костанайская", "Акмолинская", "Актюбинская", "Западно-Казахстанская", "Атырауская", "Карагандинская", "Туркестанская", "ДПБО", "ДПКК"];
|
||
currentSheet = currentSheet || "all";
|
||
|
||
const tabsContainer = document.getElementById("sheetTabs");
|
||
tabsContainer.innerHTML = sheets.map(s => `<div class="tab ${s===currentSheet?'active':''}" onclick="setSheet('${s}')">${s==="all"?"Все":s}</div>`).join("");
|
||
}
|
||
|
||
function setSheet(sheet) {
|
||
currentSheet = sheet;
|
||
loadTabs();
|
||
renderTable();
|
||
}
|
||
|
||
function loadTabs() {
|
||
const sheets = ["all", "Алматы", "Астана", "Шымкент", "Алматинская", "Жамбылская", "Кызылординская", "Мангистауская", "Павлодарская", "Восточно-Казахстанская", "Северо-Казахстанская", "Костанайская", "Акмолинская", "Актюбинская", "Западно-Казахстанская", "Атырауская", "Карагандинская", "Туркестанская", "ДПБО", "ДПКК"];
|
||
const tabsContainer = document.getElementById("sheetTabs");
|
||
tabsContainer.innerHTML = sheets.map(s => `<div class="tab ${s===currentSheet?'active':''}" onclick="setSheet('${s}')">${s==="all"?"Все":s}</div>`).join("");
|
||
}
|
||
|
||
function getFilteredRecords() {
|
||
let records = allRecords;
|
||
|
||
if (currentSheet !== "all") {
|
||
if (["ДПБО","ДПКК"].includes(currentSheet)) {
|
||
records = records.filter(r => r.segment === currentSheet);
|
||
} else {
|
||
records = records.filter(r => r.region === currentSheet);
|
||
}
|
||
}
|
||
|
||
const search = document.getElementById("searchInput").value.toLowerCase();
|
||
if (search) {
|
||
records = records.filter(r =>
|
||
(r.clientName||"").toLowerCase().includes(search) ||
|
||
(r.address||"").toLowerCase().includes(search) ||
|
||
(r.num||"").toLowerCase().includes(search)
|
||
);
|
||
}
|
||
|
||
const region = document.getElementById("filterRegion").value;
|
||
if (region) records = records.filter(r => r.region === region);
|
||
|
||
const status = document.getElementById("filterStatus").value;
|
||
if (status) records = records.filter(r => r.connectionStatus === status);
|
||
|
||
return records;
|
||
}
|
||
|
||
function renderTable() {
|
||
const records = getFilteredRecords();
|
||
const tbody = document.getElementById("tableBody");
|
||
|
||
if (records.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="19" style="text-align:center;padding:40px">Нет записей</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = records.map(r => {
|
||
const canEdit = currentUser && (currentUser.role === "admin" || canEditRecord(r));
|
||
return '<tr>' + [
|
||
r.num||"", r.segment||"", r.region||"", r.district||"", r.settlement||"", r.address||"",
|
||
r.clientName||"", r.activity||"", r.techPossibility||"", r.constructionStatus||"",
|
||
formatDate(r.constructionStart), formatDate(r.constructionEnd), r.note||"",
|
||
r.deviceNum||"", r.connectionStatus||"", formatDate(r.connectionDate), r.tariff||"", r.arpu||""
|
||
].map(v => `<td>${v||""}</td>`).join("") +
|
||
`<td>${canEdit ? `<span class="edit-cell" onclick="editRecord(${r.id})">✏️</span>` : ""}</td>` +
|
||
'</tr>';
|
||
}).join("");
|
||
}
|
||
|
||
function formatDate(d) {
|
||
if (!d) return "";
|
||
const date = new Date(d);
|
||
return isNaN(date.getTime()) ? d : date.toLocaleDateString("ru-RU");
|
||
}
|
||
|
||
function canEditRecord(record) {
|
||
if (currentUser && currentUser.role === "admin") return true;
|
||
|
||
const perms = window.userPermissions || [];
|
||
const perm = perms.find(p => p.userName === currentUser.fullName && (p.sheetName === "" || p.sheetName === currentSheet || p.sheetName === "all"));
|
||
|
||
return perm && (perm.level === "editor" || perm.level === "curator");
|
||
}
|
||
|
||
function openModal(id = null) {
|
||
editingId = id;
|
||
const modal = document.getElementById("recordModal");
|
||
const title = document.getElementById("modalTitle");
|
||
|
||
FIELDS.forEach(f => document.getElementById("rec" + f.charAt(0).toUpperCase() + f.slice(1)).value = "");
|
||
|
||
if (id) {
|
||
const record = allRecords.find(r => r.id === id);
|
||
if (record) {
|
||
title.textContent = "Редактирование записи";
|
||
FIELDS.forEach(f => {
|
||
const el = document.getElementById("rec" + f.charAt(0).toUpperCase() + f.slice(1));
|
||
if (el) el.value = record[f] || "";
|
||
});
|
||
}
|
||
} else {
|
||
title.textContent = "Новая запись";
|
||
}
|
||
|
||
modal.classList.add("open");
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById("recordModal").classList.remove("open");
|
||
editingId = null;
|
||
}
|
||
|
||
async function saveRecord() {
|
||
const record = {};
|
||
FIELDS.forEach(f => {
|
||
const el = document.getElementById("rec" + f.charAt(0).toUpperCase() + f.slice(1));
|
||
if (el) record[f] = el.value;
|
||
});
|
||
|
||
const canEdit = currentUser && (currentUser.role === "admin" || canEditRecord({}));
|
||
if (!canEdit && !editingId) {
|
||
return alert("У вас нет прав на добавление записей. Обратитесь к администратору.");
|
||
}
|
||
if (!canEdit && editingId) {
|
||
return alert("У вас нет прав на редактирование. Обратитесь к администратору.");
|
||
}
|
||
|
||
if (editingId) {
|
||
record.id = editingId;
|
||
await fetch("/api/records", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(record) });
|
||
} else {
|
||
await fetch("/api/records", { method: "POST", headers: {"Content-Type":"application/json"}, body: JSON.stringify(record) });
|
||
}
|
||
|
||
closeModal();
|
||
await loadRecords();
|
||
await loadUserPermissions();
|
||
renderTable();
|
||
}
|
||
|
||
async function editRecord(id) {
|
||
openModal(id);
|
||
}
|
||
|
||
async function exportExcel() {
|
||
window.open("/api/export/excel", "_blank");
|
||
}
|
||
|
||
async function openStatsModal() {
|
||
document.getElementById("statsModal").classList.add("open");
|
||
}
|
||
|
||
async function loadStats() {
|
||
const startDate = document.getElementById("statsStartDate").value;
|
||
const endDate = document.getElementById("statsEndDate").value;
|
||
const groupBy = document.getElementById("statsGroupBy").value;
|
||
|
||
const res = await fetch("/api/stats", {
|
||
method: "POST",
|
||
headers: {"Content-Type":"application/json"},
|
||
body: JSON.stringify({ startDate, endDate, groupBy })
|
||
});
|
||
const data = await res.json();
|
||
|
||
const grid = document.getElementById("statsGrid");
|
||
grid.innerHTML = Object.entries(data.stats).map(([key, count]) =>
|
||
`<div class="stat-card"><h3>${key}</h3><div class="value">${count}</div></div>`
|
||
).join("");
|
||
}
|
||
|
||
async function openPermissionsModal() {
|
||
document.getElementById("permissionsModal").classList.add("open");
|
||
await loadPermissions();
|
||
await loadUsersForPermissions();
|
||
await loadSheetsForPermissions();
|
||
}
|
||
|
||
async function loadUsersForPermissions() {
|
||
const res = await fetch("/api/users");
|
||
const data = await res.json();
|
||
const select = document.getElementById("permUser");
|
||
select.innerHTML = data.users.map(u => `<option value="${u.id || u.fullName}">${u.fullName || u.name} (${u.branch || '-'})</option>`).join("");
|
||
}
|
||
|
||
async function loadSheetsForPermissions() {
|
||
const sheets = ["all", "Алматы", "Астана", "Шымкент", "Алматинская", "Жамбылская", "Кызылординская", "Мангистауская", "Павлодарская", "Восточно-Казахстанская", "Северо-Казахстанская", "Костанайская", "Акмолинская", "Актюбинская", "Западно-Казахстанская", "Атырауская", "Карагандинская", "Туркестанская", "ДПБО", "ДПКК"];
|
||
const select = document.getElementById("permSheet");
|
||
select.innerHTML = sheets.map(s => `<option value="${s}">${s==="all"?"Все листы":s}</option>`).join("");
|
||
}
|
||
|
||
async function loadPermissions() {
|
||
const res = await fetch("/api/permissions");
|
||
const data = await res.json();
|
||
const usersRes = await fetch("/api/users");
|
||
const usersData = await usersRes.json();
|
||
const list = document.getElementById("permissionsList");
|
||
list.innerHTML = data.permissions.map(p => {
|
||
const user = usersData.users.find(u => u.fullName === p.userName);
|
||
return `<div style="padding:12px;background:var(--gray-100);border-radius:8px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
||
<span><b>${p.userName}</b> ${user ? `(${user.branch || '-'})` : ''} → ${p.sheetName || "Все листы"} (<b>${p.level === 'curator' ? 'Куратор' : p.level === 'editor' ? 'Редактор' : 'Просмотр'}</b>)</span>
|
||
<button class="btn btn-danger" onclick="deletePermission(${p.id})">Удалить</button>
|
||
</div>`;
|
||
}).join("");
|
||
}
|
||
|
||
async function addPermission() {
|
||
const userId = document.getElementById("permUser").value;
|
||
const sheet = document.getElementById("permSheet").value;
|
||
const level = document.getElementById("permLevel").value;
|
||
|
||
const usersRes = await fetch("/api/users");
|
||
const usersData = await usersRes.json();
|
||
const user = usersData.users.find(u => (u.id || u.fullName) === userId);
|
||
const userName = user ? user.fullName : userId;
|
||
|
||
await fetch("/api/permissions", {
|
||
method: "POST",
|
||
headers: {"Content-Type":"application/json"},
|
||
body: JSON.stringify({ userId, userName, sheetName: sheet === "all" ? "" : sheet, level })
|
||
});
|
||
|
||
loadPermissions();
|
||
}
|
||
|
||
async function deletePermission(id) {
|
||
await fetch("/api/permissions", { method: "DELETE", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ id }) });
|
||
loadPermissions();
|
||
}
|
||
|
||
async function openUsersModal() {
|
||
document.getElementById("usersModal").classList.add("open");
|
||
await loadUsersList();
|
||
}
|
||
|
||
async function loadUsersList() {
|
||
const res = await fetch("/api/users");
|
||
const data = await res.json();
|
||
const list = document.getElementById("usersList");
|
||
list.innerHTML = data.users.map(u =>
|
||
`<div style="padding:12px;background:var(--gray-100);border-radius:8px;margin-bottom:8px;display:flex;flex-direction:column;gap:8px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span><b>${u.fullName || u.name}</b> (${u.branch || ""} | ${u.department || ""})</span>
|
||
<span class="permission-badge">${u.approvedRole === 'curator' ? 'Куратор' : u.approvedRole === 'editor' ? 'Редактор' : 'Просмотр'}${u.requestedRole !== u.approvedRole ? ' (запрос: ' + (u.requestedRole === 'curator' ? 'Куратор' : u.requestedRole === 'editor' ? 'Редактор' : 'Просмотр') + ')' : ''}</span>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||
<span style="font-size:12px;color:var(--gray-500)">Тел: ${u.phone || '-'}</span>
|
||
<span style="font-size:12px;color:var(--gray-500)">Должность: ${u.position || '-'}</span>
|
||
<select onchange="updateUserRole(${u.id || u.fullName}, this.value)" style="padding:6px;border-radius:6px;border:1px solid var(--gray-100)">
|
||
<option value="view" ${u.approvedRole === 'view' ? 'selected' : ''}>Просмотр</option>
|
||
<option value="editor" ${u.approvedRole === 'editor' ? 'selected' : ''}>Редактор</option>
|
||
<option value="curator" ${u.approvedRole === 'curator' ? 'selected' : ''}>Куратор</option>
|
||
</select>
|
||
<button class="btn btn-danger" onclick="deleteUser(${u.id || '${u.fullName}'})">Удалить</button>
|
||
</div>
|
||
</div>`
|
||
).join("");
|
||
}
|
||
|
||
async function updateUserRole(userId, newRole) {
|
||
const res = await fetch("/api/users");
|
||
const data = await res.json();
|
||
const user = data.users.find(u => (u.id || u.fullName) === userId || u.id === userId);
|
||
if (user) {
|
||
user.approvedRole = newRole;
|
||
await fetch("/api/users", { method: "PUT", headers: {"Content-Type":"application/json"}, body: JSON.stringify(user) });
|
||
|
||
const permsRes = await fetch("/api/permissions");
|
||
const permsData = await permsRes.json();
|
||
const existingPerm = permsData.permissions.find(p => p.userName === user.fullName);
|
||
if (existingPerm) {
|
||
await fetch("/api/permissions", { method: "DELETE", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ id: existingPerm.id }) });
|
||
}
|
||
await fetch("/api/permissions", {
|
||
method: "POST",
|
||
headers: {"Content-Type":"application/json"},
|
||
body: JSON.stringify({ userId: user.fullName, userName: user.fullName, sheetName: "", level: newRole })
|
||
});
|
||
}
|
||
loadUsersList();
|
||
}
|
||
|
||
async function addUser() {
|
||
const name = document.getElementById("newUserName").value;
|
||
const role = document.getElementById("newUserRole").value;
|
||
|
||
if (!name) return alert("Введите имя");
|
||
|
||
await fetch("/api/users", {
|
||
method: "POST",
|
||
headers: {"Content-Type":"application/json"},
|
||
body: JSON.stringify({ name, role })
|
||
});
|
||
|
||
document.getElementById("newUserName").value = "";
|
||
loadUsersList();
|
||
}
|
||
|
||
async function deleteUser(id) {
|
||
await fetch("/api/users", { method: "DELETE", headers: {"Content-Type":"application/json"}, body: JSON.stringify({ id }) });
|
||
loadUsersList();
|
||
}
|
||
|
||
document.addEventListener("keydown", e => {
|
||
if (e.key === "Escape") {
|
||
document.querySelectorAll(".modal.open").forEach(m => m.classList.remove("open"));
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|