Добавлена простая аутентификация: login.html, /api/login, /api/logout, сессии
This commit is contained in:
parent
bd48056979
commit
dd49981307
248
index.html
248
index.html
@ -4,66 +4,220 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Название проекта</title>
|
||||
<title>Реестр приказов АО</title>
|
||||
<link rel="icon" href="design-system/brand/kt-ai-logo.png">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||||
<style>
|
||||
/* ДС рисует кнопки для <button>; у ссылки-кнопки браузер добавляет подчёркивание */
|
||||
a.kt-ai-btn { text-decoration: none; }
|
||||
@layer product {
|
||||
.kpi-strip { display: flex; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg-soft); }
|
||||
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-lg); padding: var(--kt-ai-space-4); min-width: 140px; }
|
||||
.kpi-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
|
||||
.kpi-label { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
|
||||
.toolbar { padding: var(--kt-ai-space-4) var(--kt-ai-space-5); display: flex; gap: var(--kt-ai-space-3); align-items: center; flex-wrap: wrap; }
|
||||
.search-input { width: 280px; }
|
||||
.filter-chip { padding: var(--kt-ai-space-1) var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-sm); border: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg); cursor: pointer; }
|
||||
.filter-chip.is-active { background: var(--kt-ai-primary); color: white; border-color: var(--kt-ai-primary); }
|
||||
.table-wrap { padding: 0 var(--kt-ai-space-5); overflow: auto; max-height: calc(100vh - 220px); }
|
||||
.status-pill { display: inline-block; padding: var(--kt-ai-space-1) var(--kt-ai-space-2); border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
|
||||
.status-active { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
|
||||
.status-expired { background: var(--kt-ai-status-risk-bg); color: var(--kt-ai-status-risk-fg); }
|
||||
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; display: none; }
|
||||
.drawer-overlay.open { display: block; }
|
||||
.drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 480px; background: var(--kt-ai-bg); border-left: 1px solid var(--kt-ai-border); z-index: 101; transform: translateX(100%); transition: transform 0.3s ease; }
|
||||
.drawer.open { transform: translateX(0); }
|
||||
.drawer-header { padding: var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); display: flex; justify-content: space-between; align-items: flex-start; }
|
||||
.drawer-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
|
||||
.drawer-meta { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
|
||||
.drawer-content { padding: var(--kt-ai-space-5); overflow-y: auto; max-height: calc(100vh - 80px); }
|
||||
.drawer-actions { padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; }
|
||||
.detail-row { margin-bottom: var(--kt-ai-space-4); }
|
||||
.detail-label { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: var(--kt-ai-space-1); }
|
||||
.detail-value { font-size: var(--kt-ai-text-sm); line-height: 1.6; }
|
||||
.btn-icon { background: none; border: none; cursor: pointer; padding: var(--kt-ai-space-1); border-radius: var(--kt-ai-radius-md); color: var(--kt-ai-fg-muted); }
|
||||
.btn-icon:hover { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||||
.stats-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--kt-ai-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-6); z-index: 200; width: 90%; max-width: 900px; max-height: 90vh; overflow: auto; display: none; }
|
||||
.stats-modal.open { display: block; }
|
||||
.stats-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-5); }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-5); }
|
||||
.chart-card { background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); padding: var(--kt-ai-space-4); }
|
||||
.chart-title { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); }
|
||||
.type-badge { display: inline-block; padding: 2px 8px; border-radius: var(--kt-ai-radius-md); font-size: var(--kt-ai-text-2xs); font-weight: var(--kt-ai-weight-semibold); text-transform: uppercase; }
|
||||
.type-prikaz { background: var(--kt-ai-info-bg); color: var(--kt-ai-info-fg); }
|
||||
.type-rasp { background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); }
|
||||
.type-utv { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
|
||||
.type-resh { background: var(--kt-ai-status-warn-bg); color: var(--kt-ai-status-warn-fg); }
|
||||
.user-menu { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-2) var(--kt-ai-space-3); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-lg); }
|
||||
.user-name { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
|
||||
.user-role { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="kt-ai-app">
|
||||
|
||||
<header class="kt-ai-pagehead">
|
||||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||
<strong>Название</strong>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<a class="kt-ai-btn" data-variant="ghost" href="#about">О нас</a>
|
||||
<a class="kt-ai-btn" data-variant="primary" href="#contacts">Оставить заявку</a>
|
||||
<div id="app" style="display:none">
|
||||
<div class="kt-ai-shell">
|
||||
<aside class="kt-ai-sidebar">
|
||||
<div class="sb-top">
|
||||
<img src="design-system/brand/kt-ai-logo.png" alt="KT AI" style="width:22px;height:22px;border-radius:var(--kt-ai-radius-md)">
|
||||
<span class="nm">Реестр приказов АО</span>
|
||||
</div>
|
||||
<button class="kt-ai-btn sb-new" data-variant="primary" id="addBtnSidebar">
|
||||
<svg class="kt-icon"><use href="#plus"></use></svg>
|
||||
<span>Новый документ</span>
|
||||
</button>
|
||||
<nav>
|
||||
<div class="kt-ai-nav-group">
|
||||
<div class="nav-label">Навигация</div>
|
||||
<a class="kt-ai-nav-item is-active" href="#" data-view="all">
|
||||
<svg class="kt-icon"><use href="#file"></use></svg>
|
||||
<span>Все приказы</span>
|
||||
</a>
|
||||
<a class="kt-ai-nav-item" href="#" id="openStats">
|
||||
<svg class="kt-icon"><use href="#chart"></use></svg>
|
||||
<span>Статистика</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="kt-ai-nav-group">
|
||||
<div class="nav-label">Филиалы</div>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="all"><svg class="kt-icon"><use href="#building"></use></svg><span>Все филиалы</span></a>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="ЦА"><span>ЦА</span></a>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="Алматы"><span>Алматы</span></a>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="Астана"><span>Астана</span></a>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="Шымкент"><span>Шымкент</span></a>
|
||||
<a class="kt-ai-nav-item" href="#" data-branch="Караганда"><span>Караганда</span></a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="kt-ai-nav-group" style="margin-top:auto">
|
||||
<div class="user-menu">
|
||||
<svg class="kt-icon"><use href="#user"></use></svg>
|
||||
<div>
|
||||
<div class="user-name" id="userName">Пользователь</div>
|
||||
<div class="user-role" id="userRole">Роль</div>
|
||||
</div>
|
||||
<button class="btn-icon" id="logoutBtn" style="margin-left:auto" title="Выйти">
|
||||
<svg class="kt-icon"><use href="#logout"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="kt-ai-main">
|
||||
<header class="kt-ai-topbar">
|
||||
<div class="topbar-title">
|
||||
<span class="t">Реестр приказов и распоряжений</span>
|
||||
</div>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<button class="kt-ai-btn" data-variant="primary" id="addBtn">
|
||||
<svg class="kt-icon"><use href="#plus"></use></svg>
|
||||
<span>Новый документ</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-total">0</div>
|
||||
<div class="kpi-label">Всего документов</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-active">0</div>
|
||||
<div class="kpi-label">Действует</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-month">0</div>
|
||||
<div class="kpi-label">За месяц</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value" id="kpi-branches">0</div>
|
||||
<div class="kpi-label">Филиалов</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<input class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по номеру, названию...">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<span style="font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted)">Тип:</span>
|
||||
<button class="filter-chip is-active" data-type="all">Все</button>
|
||||
<button class="filter-chip" data-type="Приказ">Приказы</button>
|
||||
<button class="filter-chip" data-type="Распоряжение">Распоряжения</button>
|
||||
<button class="filter-chip" data-type="Утверждение">Утверждения</button>
|
||||
<button class="filter-chip" data-type="Решение правления">Решения ПР</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="kt-ai-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тип</th>
|
||||
<th>Номер</th>
|
||||
<th>Дата</th>
|
||||
<th>Наименование</th>
|
||||
<th>Филиал</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersBody"></tbody>
|
||||
</table>
|
||||
<div class="kt-ai-empty" id="emptyState" style="display:none;padding:var(--kt-ai-space-12)">
|
||||
<svg class="kt-icon" style="width:48px;height:48px;margin-bottom:16px"><use href="#file"></use></svg>
|
||||
<p>Документы не найдены</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="drawer-overlay" id="drawerOverlay"></div>
|
||||
<aside class="drawer" id="drawer">
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<div class="drawer-title" id="drawerTitle">Заголовок</div>
|
||||
<div class="drawer-meta" id="drawerMeta">Номер • Дата</div>
|
||||
</div>
|
||||
<button class="btn-icon" id="closeDrawer">
|
||||
<svg class="kt-icon"><use href="#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="drawer-content" id="drawerContent"></div>
|
||||
<div class="drawer-actions">
|
||||
<button class="kt-ai-btn" data-variant="ghost" id="editBtn">Изменить</button>
|
||||
<button class="kt-ai-btn" data-variant="danger" id="deleteBtn">Удалить</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="kt-ai-hero" data-align="center">
|
||||
<div class="kt-ai-wrap">
|
||||
<h1>Главное обещание в одну строку</h1>
|
||||
<p class="kt-ai-hero-sub">Подзаголовок: кому это и почему стоит остаться.</p>
|
||||
<div class="kt-ai-hero-actions">
|
||||
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#contacts">Оставить заявку</a>
|
||||
<a class="kt-ai-btn" data-size="lg" href="#about">Подробнее</a>
|
||||
<div class="stats-modal" id="statsModal">
|
||||
<div class="stats-header">
|
||||
<h2 style="margin:0;font-size:var(--kt-ai-text-xl)">Статистика приказов</h2>
|
||||
<button class="btn-icon" id="closeStats">
|
||||
<svg class="kt-icon"><use href="#x"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">По типам документов</div>
|
||||
<canvas id="chartTypes"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">По филиалам</div>
|
||||
<canvas id="chartBranches"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">По месяцам</div>
|
||||
<canvas id="chartMonths"></canvas>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">По статусам</div>
|
||||
<canvas id="chartStatus"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="about" class="kt-ai-section" data-divider="true">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Что вы получите</h2>
|
||||
<p>Одно-два предложения пояснения к секции.</p>
|
||||
</div>
|
||||
<div class="kt-ai-grid" style="margin-top:var(--kt-ai-space-9)">
|
||||
<article class="kt-ai-tile"><h3>Пункт первый</h3><p>Коротко и по делу.</p></article>
|
||||
<article class="kt-ai-tile"><h3>Пункт второй</h3><p>Коротко и по делу.</p></article>
|
||||
<article class="kt-ai-tile"><h3>Пункт третий</h3><p>Коротко и по делу.</p></article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contacts" class="kt-ai-cta">
|
||||
<div class="kt-ai-wrap">
|
||||
<h2>Призыв к действию</h2>
|
||||
<p>Город, адрес, часы работы — то, что нужно, чтобы дойти.</p>
|
||||
<a class="kt-ai-btn" data-variant="primary" data-size="lg" href="#">Написать</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||||
<span>© 2026 Название</span>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<span>Контакты</span>
|
||||
</footer>
|
||||
<script src="design-system/icons/kt-ai-icons.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
93
login.html
Normal file
93
login.html
Normal file
@ -0,0 +1,93 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-theme="auto">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Вход в систему</title>
|
||||
<link rel="icon" href="design-system/brand/kt-ai-logo.png">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||||
<style>
|
||||
.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--kt-ai-bg); }
|
||||
.login-card { width: 100%; max-width: 400px; padding: var(--kt-ai-space-8); }
|
||||
.login-logo { text-align: center; margin-bottom: var(--kt-ai-space-6); }
|
||||
.login-logo img { width: 48px; height: 48px; border-radius: var(--kt-ai-radius-xl); margin-bottom: var(--kt-ai-space-3); }
|
||||
.login-logo h1 { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin: 0; }
|
||||
.login-logo p { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-2); }
|
||||
.login-form { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); }
|
||||
.login-hint { margin-top: var(--kt-ai-space-4); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); text-align: center; }
|
||||
.login-hint code { background: var(--kt-ai-bg-soft); padding: 2px 6px; border-radius: var(--kt-ai-radius-md); }
|
||||
.error-msg { color: var(--kt-ai-status-risk-fg); font-size: var(--kt-ai-text-sm); margin-top: var(--kt-ai-space-2); display: none; }
|
||||
.error-msg.visible { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<div class="login-logo">
|
||||
<img src="design-system/brand/kt-ai-logo.png" alt="KT AI">
|
||||
<h1>Реестр приказов АО</h1>
|
||||
<p>Войдите для продолжения</p>
|
||||
</div>
|
||||
<form class="login-form" id="loginForm">
|
||||
<div class="kt-ai-field">
|
||||
<label class="kt-ai-label">Логин</label>
|
||||
<input class="kt-ai-input" name="login" placeholder="Введите логин" required autocomplete="username">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label class="kt-ai-label">Пароль</label>
|
||||
<input class="kt-ai-input" name="password" type="password" placeholder="Введите пароль" required autocomplete="current-password">
|
||||
</div>
|
||||
<button class="kt-ai-btn" data-variant="primary" type="submit" style="width:100%;justify-content:center">
|
||||
Войти
|
||||
</button>
|
||||
<div class="error-msg" id="errorMsg"></div>
|
||||
</form>
|
||||
<div class="login-hint">
|
||||
Тестовые пользователи:<br>
|
||||
<code>admin / admin123</code> • <code>user / user123</code> • <code>filial / filial123</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="design-system/icons/kt-ai-icons.js"></script>
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const errorEl = document.getElementById('errorMsg');
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Вход...';
|
||||
errorEl.classList.remove('visible');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
login: form.login.value,
|
||||
password: form.password.value
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
errorEl.textContent = data.error || 'Ошибка входа';
|
||||
errorEl.classList.add('visible');
|
||||
}
|
||||
} catch (err) {
|
||||
errorEl.textContent = 'Ошибка подключения к серверу';
|
||||
errorEl.classList.add('visible');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Войти';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
package-lock.json
generated
Normal file
10
package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "ao-orders-app",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ao-orders-app"
|
||||
}
|
||||
}
|
||||
}
|
||||
46
script.js
46
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);
|
||||
});
|
||||
|
||||
71
server.js
71
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" });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user