Опубликовано через кнопку
This commit is contained in:
parent
454a11ce94
commit
bd721f2c66
46
auth.js
Normal file
46
auth.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Проверка авторизации
|
||||||
|
function getCurrentUser() {
|
||||||
|
const saved = localStorage.getItem("sdCurrentUser");
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuth() {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
window.location = "login.html";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canEdit() {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
return user && (user.role === "admin" || user.role === "pmo");
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDelete() {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
return user && user.role === "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem("sdCurrentUser");
|
||||||
|
window.location = "login.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUserMenu() {
|
||||||
|
const user = getCurrentUser();
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const nav = document.querySelector(".nav");
|
||||||
|
if (nav && !document.getElementById("userMenu")) {
|
||||||
|
const menu = document.createElement("span");
|
||||||
|
menu.id = "userMenu";
|
||||||
|
menu.style.cssText = "margin-left:24px;display:flex;align-items:center;gap:12px";
|
||||||
|
menu.innerHTML = `
|
||||||
|
<span style="color:rgba(255,255,255,0.8);font-size:14px">${user.name} (${user.role === "admin" ? "Админ" : user.role === "pmo" ? "ПО" : user.role === "rm" ? "РМ" : "Исполнитель"})</span>
|
||||||
|
<button onclick="logout()" style="background:rgba(255,255,255,0.2);border:none;color:white;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:13px">Выйти</button>
|
||||||
|
`;
|
||||||
|
nav.appendChild(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
card.html
59
card.html
@ -11,10 +11,10 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1 id="pageTitle">Карточка поручения</h1>
|
<h1 id="pageTitle">Карточка поручения</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav">
|
<nav class="nav" id="mainNav">
|
||||||
<a href="index.html">Дашборд</a>
|
<a href="index.html">Дашборд</a>
|
||||||
<a href="registry.html">Реестр</a>
|
<a href="registry.html">Реестр</a>
|
||||||
<a href="card.html?id=new">Новое поручение</a>
|
<a href="card.html?id=new" id="newBtn" style="display:none">Новое поручение</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
<button type="submit" class="btn btn-primary" id="saveBtn">Сохранить</button>
|
||||||
<button type="button" class="btn btn-secondary" onclick="window.location='registry.html'">Отмена</button>
|
<button type="button" class="btn btn-secondary" onclick="window.location='registry.html'">Отмена</button>
|
||||||
<button type="button" class="btn btn-danger" id="deleteBtn" onclick="deleteInstruction()" style="display:none">Удалить</button>
|
<button type="button" class="btn btn-danger" id="deleteBtn" onclick="deleteInstruction()" style="display:none">Удалить</button>
|
||||||
</div>
|
</div>
|
||||||
@ -101,17 +101,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="auth.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const user = requireAuth();
|
||||||
|
renderUserMenu();
|
||||||
|
|
||||||
|
// Права на редактирование
|
||||||
|
const canEditAccess = canEdit();
|
||||||
|
const canDeleteAccess = canDelete();
|
||||||
|
|
||||||
|
if (canEditAccess) {
|
||||||
|
document.getElementById("newBtn").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const instructionId = urlParams.get("id");
|
const instructionId = urlParams.get("id");
|
||||||
let instruction = null;
|
let instruction = null;
|
||||||
|
|
||||||
|
// Блокируем форму для тех, кто не может редактировать
|
||||||
|
if (!canEditAccess && instructionId !== "new") {
|
||||||
|
document.getElementById("title").disabled = true;
|
||||||
|
document.getElementById("status").disabled = true;
|
||||||
|
document.getElementById("rm").disabled = true;
|
||||||
|
document.getElementById("issueDate").disabled = true;
|
||||||
|
document.getElementById("planDate").disabled = true;
|
||||||
|
document.getElementById("factDate").disabled = true;
|
||||||
|
document.getElementById("delegate").disabled = true;
|
||||||
|
document.getElementById("note").disabled = true;
|
||||||
|
document.getElementById("saveBtn").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
if (instructionId === "new") {
|
if (instructionId === "new") {
|
||||||
|
if (!canEditAccess) {
|
||||||
|
alert("У вас нет прав на создание поручений");
|
||||||
|
window.location = "index.html";
|
||||||
|
return;
|
||||||
|
}
|
||||||
document.getElementById("formTitle").textContent = "Новое поручение";
|
document.getElementById("formTitle").textContent = "Новое поручение";
|
||||||
document.getElementById("deleteBtn").style.display = "none";
|
document.getElementById("deleteBtn").style.display = "none";
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
document.getElementById("issueDate").value = today;
|
document.getElementById("issueDate").value = today;
|
||||||
|
document.getElementById("answerAuthor").value = user.name;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +165,11 @@
|
|||||||
document.getElementById("factDate").value = instruction.factDate || "";
|
document.getElementById("factDate").value = instruction.factDate || "";
|
||||||
document.getElementById("delegate").value = instruction.delegate || "";
|
document.getElementById("delegate").value = instruction.delegate || "";
|
||||||
document.getElementById("note").value = instruction.note || "";
|
document.getElementById("note").value = instruction.note || "";
|
||||||
document.getElementById("deleteBtn").style.display = "inline-block";
|
|
||||||
|
if (canDeleteAccess) {
|
||||||
|
document.getElementById("deleteBtn").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
document.getElementById("answerAuthor").value = user.name;
|
||||||
|
|
||||||
// Answers
|
// Answers
|
||||||
if (instruction.answers && instruction.answers.length) {
|
if (instruction.answers && instruction.answers.length) {
|
||||||
@ -174,6 +209,11 @@
|
|||||||
|
|
||||||
document.getElementById("instructionForm").addEventListener("submit", async (e) => {
|
document.getElementById("instructionForm").addEventListener("submit", async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!canEditAccess) {
|
||||||
|
alert("У вас нет прав на редактирование");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
title: document.getElementById("title").value,
|
title: document.getElementById("title").value,
|
||||||
status: document.getElementById("status").value,
|
status: document.getElementById("status").value,
|
||||||
@ -183,7 +223,7 @@
|
|||||||
factDate: document.getElementById("factDate").value,
|
factDate: document.getElementById("factDate").value,
|
||||||
delegate: document.getElementById("delegate").value,
|
delegate: document.getElementById("delegate").value,
|
||||||
note: document.getElementById("note").value,
|
note: document.getElementById("note").value,
|
||||||
user: "Пользователь"
|
user: user.name
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = instructionId === "new" ? "/api/instructions" : `/api/instructions/${instructionId}`;
|
const url = instructionId === "new" ? "/api/instructions" : `/api/instructions/${instructionId}`;
|
||||||
@ -204,8 +244,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function addAnswer() {
|
async function addAnswer() {
|
||||||
|
if (!instructionId || instructionId === "new") return;
|
||||||
const text = document.getElementById("answerText").value;
|
const text = document.getElementById("answerText").value;
|
||||||
const author = document.getElementById("answerAuthor").value || "Автор";
|
const author = document.getElementById("answerAuthor").value || user.name;
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
|
||||||
const res = await fetch(`/api/instructions/${instructionId}`, {
|
const res = await fetch(`/api/instructions/${instructionId}`, {
|
||||||
@ -213,7 +254,7 @@
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
answer: { text, author },
|
answer: { text, author },
|
||||||
user: author
|
user: user.name
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -226,6 +267,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInstruction() {
|
async function deleteInstruction() {
|
||||||
|
if (!canDeleteAccess) {
|
||||||
|
alert("У вас нет прав на удаление");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!confirm("Удалить это поручение?")) return;
|
if (!confirm("Удалить это поручение?")) return;
|
||||||
const res = await fetch(`/api/instructions/${instructionId}`, { method: "DELETE" });
|
const res = await fetch(`/api/instructions/${instructionId}`, { method: "DELETE" });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
14
index.html
14
index.html
@ -11,10 +11,10 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Контроль исполнения поручений Совета директоров</h1>
|
<h1>Контроль исполнения поручений Совета директоров</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav">
|
<nav class="nav" id="mainNav">
|
||||||
<a href="index.html" class="active">Дашборд</a>
|
<a href="index.html" class="active">Дашборд</a>
|
||||||
<a href="registry.html">Реестр</a>
|
<a href="registry.html">Реестр</a>
|
||||||
<a href="card.html?id=new">Новое поручение</a>
|
<a href="card.html?id=new" id="newBtn" style="display:none">Новое поручение</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -76,7 +76,17 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="auth.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
// Проверка авторизации
|
||||||
|
const user = requireAuth();
|
||||||
|
renderUserMenu();
|
||||||
|
|
||||||
|
// Показываем кнопку "Новое поручение" только для admin/pmo
|
||||||
|
if (canEdit()) {
|
||||||
|
document.getElementById("newBtn").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
let allInstructions = [];
|
let allInstructions = [];
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
|||||||
165
login.html
Normal file
165
login.html
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Вход в систему</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
.login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
|
||||||
|
}
|
||||||
|
.login-box {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.login-box h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1e3a5f;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.login-box .subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.login-box .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.login-box label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.login-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.login-box button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.login-box button:hover {
|
||||||
|
background: #2c5282;
|
||||||
|
}
|
||||||
|
.login-box .error {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.demo-users {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.demo-users h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.demo-users table {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.demo-users td {
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.demo-users td:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e3a5f;
|
||||||
|
}
|
||||||
|
.demo-users td:last-child {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="login-box">
|
||||||
|
<h1>Контроль исполнения поручений СД</h1>
|
||||||
|
<p class="subtitle">Войдите для продолжения работы</p>
|
||||||
|
|
||||||
|
<div class="error" id="errorMsg"></div>
|
||||||
|
|
||||||
|
<form id="loginForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="login">Логин</label>
|
||||||
|
<input type="text" id="login" name="login" required placeholder="Введите логин">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" id="password" name="password" required placeholder="Введите пароль">
|
||||||
|
</div>
|
||||||
|
<button type="submit">Войти</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="demo-users">
|
||||||
|
<h3>Тестовые пользователи:</h3>
|
||||||
|
<table>
|
||||||
|
<tr><td>admin</td><td>admin</td><td>Администратор</td></tr>
|
||||||
|
<tr><td>pmo</td><td>pmo</td><td>Сотрудник ПО</td></tr>
|
||||||
|
<tr><td>ivanov</td><td>123</td><td>РМ</td></tr>
|
||||||
|
<tr><td>executor</td><td>123</td><td>Исполнитель</td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Проверка: если уже авторизован — редирект на дашборд
|
||||||
|
const saved = localStorage.getItem("sdCurrentUser");
|
||||||
|
if (saved) {
|
||||||
|
window.location = "index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("loginForm").addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const login = document.getElementById("login").value;
|
||||||
|
const password = document.getElementById("password").value;
|
||||||
|
|
||||||
|
const res = await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ login, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.ok) {
|
||||||
|
localStorage.setItem("sdCurrentUser", JSON.stringify(result.user));
|
||||||
|
window.location = "index.html";
|
||||||
|
} else {
|
||||||
|
document.getElementById("errorMsg").textContent = result.error || "Ошибка входа";
|
||||||
|
document.getElementById("errorMsg").style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -11,10 +11,10 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1>Реестр поручений Совета директоров</h1>
|
<h1>Реестр поручений Совета директоров</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="nav">
|
<nav class="nav" id="mainNav">
|
||||||
<a href="index.html">Дашборд</a>
|
<a href="index.html">Дашборд</a>
|
||||||
<a href="registry.html" class="active">Реестр</a>
|
<a href="registry.html" class="active">Реестр</a>
|
||||||
<a href="card.html?id=new">Новое поручение</a>
|
<a href="card.html?id=new" id="newBtn" style="display:none">Новое поручение</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -41,7 +41,15 @@
|
|||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<script src="auth.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
const user = requireAuth();
|
||||||
|
renderUserMenu();
|
||||||
|
|
||||||
|
if (canEdit()) {
|
||||||
|
document.getElementById("newBtn").style.display = "inline-block";
|
||||||
|
}
|
||||||
|
|
||||||
let allInstructions = [];
|
let allInstructions = [];
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
|||||||
19
server.js
19
server.js
@ -147,6 +147,25 @@ http.createServer(async (req, res) => {
|
|||||||
return res.end(JSON.stringify(data.users));
|
return res.end(JSON.stringify(data.users));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: Логин
|
||||||
|
if (req.method === "POST" && url === "/api/login") {
|
||||||
|
const data = readData();
|
||||||
|
const { login, password } = await body(req);
|
||||||
|
const user = data.users.find(u => u.login === login && u.password === password);
|
||||||
|
if (!user) {
|
||||||
|
res.writeHead(401, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: "Неверный логин или пароль" }));
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: true, user: { login: user.login, name: user.name, role: user.role } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// API: Logout (просто для вида)
|
||||||
|
if (req.method === "POST" && url === "/api/logout") {
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
return res.end(JSON.stringify({ ok: true }));
|
||||||
|
}
|
||||||
|
|
||||||
// API: Выгрузка в CSV (Excel)
|
// API: Выгрузка в CSV (Excel)
|
||||||
if (req.method === "GET" && url === "/api/export") {
|
if (req.method === "GET" && url === "/api/export") {
|
||||||
const data = readData();
|
const data = readData();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user