-
Главное обещание в одну строку
-
Подзаголовок: кому это и почему стоит остаться.
-
-
-
-
-
Что вы получите
-
Одно-два предложения пояснения к секции.
-
-
-
Пункт первый
Коротко и по делу.
-
Пункт второй
Коротко и по делу.
-
Пункт третий
Коротко и по делу.
-
-
-
-
-
-
-
+
+
+
diff --git a/login.html b/login.html
new file mode 100644
index 0000000..5f64cef
--- /dev/null
+++ b/login.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
Вход в систему
+
+
+
+
+
+
+
+
+
+
+
+

+
Реестр приказов АО
+
Войдите для продолжения
+
+
+
+ Тестовые пользователи:
+ admin / admin123 • user / user123 • filial / filial123
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..36b9e3f
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,10 @@
+{
+ "name": "ao-orders-app",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ao-orders-app"
+ }
+ }
+}
diff --git a/script.js b/script.js
index 5c646ea..d0fc288 100644
--- a/script.js
+++ b/script.js
@@ -2,11 +2,34 @@ let allOrders = [];
let currentFilter = { branch: 'all', type: 'all', search: '' };
let selectedOrder = null;
let charts = {};
+let currentUser = null;
+
+async function checkAuth() {
+ try {
+ const res = await fetch('/api/me');
+ const data = await res.json();
+ if (data.ok) {
+ currentUser = data.user;
+ document.getElementById('userName').textContent = user.name;
+ document.getElementById('userRole').textContent = user.role;
+ document.getElementById('app').style.display = 'block';
+ loadOrders();
+ } else {
+ window.location.href = '/login.html';
+ }
+ } catch (e) {
+ window.location.href = '/login.html';
+ }
+}
async function loadOrders() {
try {
const res = await fetch('/api/items');
const data = await res.json();
+ if (data.error) {
+ window.location.href = '/login.html';
+ return;
+ }
allOrders = data.items || [];
renderTable();
updateKPI();
@@ -147,22 +170,18 @@ function renderCharts() {
const ctxMonths = document.getElementById('chartMonths').getContext('2d');
const ctxStatus = document.getElementById('chartStatus').getContext('2d');
- // По типам
const byType = {};
allOrders.forEach(o => { byType[o.type || 'Приказ'] = (byType[o.type || 'Приказ'] || 0) + 1; });
- // По филиалам
const byBranch = {};
allOrders.forEach(o => { byBranch[o.branch || 'Не указан'] = (byBranch[o.branch || 'Не указан'] || 0) + 1; });
- // По месяцам
const byMonth = {};
allOrders.forEach(o => {
const m = (o.date || '').slice(0, 7) || 'Не указана';
byMonth[m] = (byMonth[m] || 0) + 1;
});
- // По статусам
const byStatus = {};
allOrders.forEach(o => { byStatus[o.status || 'Проект'] = (byStatus[o.status || 'Проект'] || 0) + 1; });
@@ -217,17 +236,21 @@ function renderCharts() {
});
}
-// Event listeners
+async function logout() {
+ try {
+ await fetch('/api/logout', { method: 'POST' });
+ } catch (e) {}
+ window.location.href = '/login.html';
+}
+
document.addEventListener('DOMContentLoaded', () => {
- loadOrders();
+ checkAuth();
- // Поиск
document.getElementById('searchInput').addEventListener('input', (e) => {
currentFilter.search = e.target.value;
renderTable();
});
- // Фильтры по типу
document.querySelectorAll('[data-type]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-type]').forEach(b => b.classList.remove('is-active'));
@@ -237,7 +260,6 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
- // Фильтры по филиалам
document.querySelectorAll('[data-branch]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
@@ -246,11 +268,9 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
- // Открытие drawer
document.getElementById('drawerOverlay').addEventListener('click', closeDrawer);
document.getElementById('closeDrawer').addEventListener('click', closeDrawer);
- // Статистика
document.getElementById('openStats').addEventListener('click', (e) => {
e.preventDefault();
renderCharts();
@@ -261,7 +281,6 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('statsModal').classList.remove('open');
});
- // Кнопки добавления
document.getElementById('addBtn').addEventListener('click', () => {
alert('Форма добавления документа будет в следующей версии');
});
@@ -269,7 +288,6 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Форма добавления документа будет в следующей версии');
});
- // Удаление
document.getElementById('deleteBtn').addEventListener('click', async () => {
if (!selectedOrder || !confirm('Удалить документ ' + selectedOrder.number + '?')) return;
try {
@@ -284,4 +302,6 @@ document.addEventListener('DOMContentLoaded', () => {
console.error('Delete failed:', e);
}
});
+
+ document.getElementById('logoutBtn').addEventListener('click', logout);
});
diff --git a/server.js b/server.js
index 5e48145..32ad65f 100644
--- a/server.js
+++ b/server.js
@@ -6,7 +6,7 @@ const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
function readData() {
- try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { items: [], log: [] }; }
+ try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { users: [], items: [], log: [] }; }
}
function writeData(d) {
fs.writeFileSync(DATA, JSON.stringify(d, null, 2));
@@ -19,6 +19,8 @@ function body(req) {
});
}
+const sessions = new Map();
+
http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
@@ -30,47 +32,100 @@ http.createServer(async (req, res) => {
}
const url = req.url.split("?")[0];
+ const cookie = req.headers.cookie || "";
+ const sessionId = cookie.split(";").find(c => c.trim().startsWith("session="))?.split("=")[1];
+ const user = sessionId ? sessions.get(sessionId) : null;
- // API: получить все приказы
+ // API: вход
+ if (req.method === "POST" && url === "/api/login") {
+ const { login, password } = await body(req);
+ const data = readData();
+ const found = data.users.find(u => u.login === login && u.password === password);
+ if (found) {
+ const sid = Math.random().toString(36).slice(2);
+ sessions.set(sid, { id: found.id, login: found.login, name: found.name, role: found.role });
+ res.setHeader("Set-Cookie", `session=${sid}; Path=/; Max-Age=86400`);
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: true, user: { id: found.id, login: found.login, name: found.name, role: found.role } }));
+ }
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: false, error: "Неверный логин или пароль" }));
+ }
+
+ // API: выход
+ if (req.method === "POST" && url === "/api/logout") {
+ if (sessionId) sessions.delete(sessionId);
+ res.setHeader("Set-Cookie", "session=; Path=/; Max-Age=0");
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: true }));
+ }
+
+ // API: текущий пользователь
+ if (req.method === "GET" && url === "/api/me") {
+ if (user) {
+ res.writeHead(200, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: true, user }));
+ }
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ ok: false }));
+ }
+
+ // API: получить все приказы (требуется вход)
if (req.method === "GET" && url === "/api/items") {
+ if (!user) {
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ error: "Требуется авторизация" }));
+ }
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(readData()));
}
- // API: добавить приказ
+ // API: добавить приказ (требуется вход)
if (req.method === "POST" && url === "/api/items") {
+ if (!user) {
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ error: "Требуется авторизация" }));
+ }
const item = await body(req);
const data = readData();
const newItem = { id: Date.now(), ...item };
data.items.push(newItem);
- data.log.push({ when: new Date().toISOString(), what: "Добавлен приказ " + newItem.number, who: "user" });
+ data.log.push({ when: new Date().toISOString(), what: "Добавлен приказ " + newItem.number, who: user.name });
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, item: newItem }));
}
- // API: обновить приказ
+ // API: обновить приказ (требуется вход)
if (req.method === "PUT" && url === "/api/items") {
+ if (!user) {
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ error: "Требуется авторизация" }));
+ }
const item = await body(req);
const data = readData();
const idx = data.items.findIndex(i => i.id === item.id);
if (idx !== -1) {
data.items[idx] = { ...data.items[idx], ...item };
- data.log.push({ when: new Date().toISOString(), what: "Обновлён приказ " + item.number, who: "user" });
+ data.log.push({ when: new Date().toISOString(), what: "Обновлён приказ " + item.number, who: user.name });
writeData(data);
}
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
- // API: удалить приказ
+ // API: удалить приказ (требуется вход)
if (req.method === "DELETE" && url === "/api/items") {
+ if (!user) {
+ res.writeHead(401, { "Content-Type": "application/json" });
+ return res.end(JSON.stringify({ error: "Требуется авторизация" }));
+ }
const { id } = await body(req);
const data = readData();
const found = data.items.find(i => i.id === id);
data.items = data.items.filter(i => i.id !== id);
if (found) {
- data.log.push({ when: new Date().toISOString(), what: "Удалён приказ " + found.number, who: "user" });
+ data.log.push({ when: new Date().toISOString(), what: "Удалён приказ " + found.number, who: user.name });
writeData(data);
}
res.writeHead(200, { "Content-Type": "application/json" });