https-docs-google-com-spread/index.html

742 lines
35 KiB
HTML
Raw 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">
<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>