diff --git a/index.html b/index.html index ac5b17f..93ee010 100644 --- a/index.html +++ b/index.html @@ -4,66 +4,220 @@ -Название проекта +Реестр приказов АО + - - + -
-
- Название - - О нас - Оставить заявку +
+
+
+ + +
+ -
-
-

Главное обещание в одну строку

-

Подзаголовок: кому это и почему стоит остаться.

-
- Оставить заявку - Подробнее +
+
+

Статистика приказов

+ +
+
+
+
По типам документов
+ +
+
+
По филиалам
+ +
+
+
По месяцам
+ +
+
+
По статусам
+
-
+ + -
-
-
-

Что вы получите

-

Одно-два предложения пояснения к секции.

-
-
-

Пункт первый

Коротко и по делу.

-

Пункт второй

Коротко и по делу.

-

Пункт третий

Коротко и по делу.

-
-
-
- -
-
-

Призыв к действию

-

Город, адрес, часы работы — то, что нужно, чтобы дойти.

- Написать -
-
- - + + + diff --git a/login.html b/login.html new file mode 100644 index 0000000..5f64cef --- /dev/null +++ b/login.html @@ -0,0 +1,93 @@ + + + + + +Вход в систему + + + + + + + + +
+
+ + + +
+
+ + + + + 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" });