Add login flow and director-only create button
Login overlay on first visit. Pick employee from list; director requires password (demo: 1234). Session persists in localStorage. The "Create task" menu item and form are gated to the director role. Employees are redirected away from #/create. Top bar shows a user chip with avatar, name, role, and logout button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d6102f354a
commit
46f21c2ee2
102
app.js
102
app.js
@ -2,6 +2,18 @@
|
||||
|
||||
const STORAGE_KEY = 'sultan.tasks.v6';
|
||||
const USER_KEY = 'sultan.currentUser.v1';
|
||||
const SESSION_KEY = 'sultan.session.v1';
|
||||
const DIRECTOR_PASSWORD = '1234';
|
||||
|
||||
function getSession() {
|
||||
try {
|
||||
const raw = localStorage.getItem(SESSION_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
function setSession(sess) { localStorage.setItem(SESSION_KEY, JSON.stringify(sess)); }
|
||||
function clearSession() { localStorage.removeItem(SESSION_KEY); }
|
||||
function isDirector() { return state.currentUser === 'Директор'; }
|
||||
|
||||
// Очистка старых версий ключей (миграция).
|
||||
(function purgeOldKeys() {
|
||||
@ -166,7 +178,7 @@ function fillSelect(el, items, includeAll = false, allLabel = 'Все') {
|
||||
// ---------- state ----------
|
||||
|
||||
const state = {
|
||||
currentUser: localStorage.getItem(USER_KEY) || 'Директор',
|
||||
currentUser: (getSession() && getSession().user) || null,
|
||||
filters: { assignee: '', status: '', direction: '', priority: '', deadline: '' },
|
||||
view: 'home',
|
||||
};
|
||||
@ -806,6 +818,10 @@ function applyRoute() {
|
||||
let view = 'home';
|
||||
if (hash === '#/create') view = 'create';
|
||||
else if (hash === '#/analytics') view = 'analytics';
|
||||
if (view === 'create' && !isDirector()) {
|
||||
location.hash = '#/home';
|
||||
return;
|
||||
}
|
||||
state.view = view;
|
||||
document.body.dataset.view = view;
|
||||
$('#view-home').hidden = view !== 'home';
|
||||
@ -819,6 +835,79 @@ function applyRoute() {
|
||||
if (view === 'home') startHomeWidgets();
|
||||
}
|
||||
|
||||
// ---------- auth ----------
|
||||
|
||||
function initAuth() {
|
||||
const overlay = $('#loginOverlay');
|
||||
const sel = $('#loginUser');
|
||||
const pwdLabel = $('#loginPwdLabel');
|
||||
const pwd = $('#loginPwd');
|
||||
const err = $('#loginError');
|
||||
const form = $('#loginForm');
|
||||
|
||||
fillSelect(sel, EMPLOYEES);
|
||||
sel.value = 'Директор';
|
||||
const togglePwd = () => {
|
||||
pwdLabel.hidden = sel.value !== 'Директор';
|
||||
if (pwdLabel.hidden) { pwd.value = ''; err.hidden = true; }
|
||||
};
|
||||
togglePwd();
|
||||
sel.addEventListener('change', togglePwd);
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const user = sel.value;
|
||||
if (user === 'Директор' && pwd.value !== DIRECTOR_PASSWORD) {
|
||||
err.hidden = false;
|
||||
pwd.focus();
|
||||
return;
|
||||
}
|
||||
err.hidden = true;
|
||||
pwd.value = '';
|
||||
loginAs(user);
|
||||
});
|
||||
|
||||
$('#logoutBtn').addEventListener('click', logout);
|
||||
|
||||
const sess = getSession();
|
||||
if (sess && sess.user && EMPLOYEES.includes(sess.user)) {
|
||||
applySession(sess.user);
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
function loginAs(user) {
|
||||
setSession({ user, ts: Date.now() });
|
||||
applySession(user);
|
||||
}
|
||||
|
||||
function applySession(user) {
|
||||
state.currentUser = user;
|
||||
localStorage.setItem(USER_KEY, user);
|
||||
document.body.dataset.role = (user === 'Директор') ? 'director' : 'employee';
|
||||
$('#loginOverlay').hidden = true;
|
||||
$('#userChip').hidden = false;
|
||||
$('#userChipName').textContent = user;
|
||||
$('#userChipRole').textContent = (user === 'Директор') ? 'Директор' : 'Сотрудник';
|
||||
$('#userChipAvatar').textContent = user.slice(0, 1).toUpperCase();
|
||||
if (!isDirector() && location.hash === '#/create') location.hash = '#/home';
|
||||
applyRoute();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
$('#loginOverlay').hidden = false;
|
||||
$('#userChip').hidden = true;
|
||||
document.body.dataset.role = '';
|
||||
}
|
||||
|
||||
function logout() {
|
||||
clearSession();
|
||||
state.currentUser = null;
|
||||
location.hash = '#/home';
|
||||
showLogin();
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
$('#menuPanel').hidden = false;
|
||||
$('#menuBtn').setAttribute('aria-expanded', 'true');
|
||||
@ -844,14 +933,8 @@ function init() {
|
||||
fillSelect($('#fAssignee'), EMPLOYEES, true, 'Все исполнители');
|
||||
fillSelect($('#fDirection'), DIRECTIONS, true, 'Все направления');
|
||||
|
||||
// Текущий пользователь
|
||||
fillSelect($('#currentUser'), EMPLOYEES);
|
||||
$('#currentUser').value = state.currentUser;
|
||||
$('#currentUser').addEventListener('change', (e) => {
|
||||
state.currentUser = e.target.value;
|
||||
localStorage.setItem(USER_KEY, state.currentUser);
|
||||
renderAll();
|
||||
});
|
||||
// Auth
|
||||
initAuth();
|
||||
|
||||
// Дедлайн — по умолчанию +3 дня. Flatpickr — большой красивый календарь.
|
||||
const dl = new Date();
|
||||
@ -874,6 +957,7 @@ function init() {
|
||||
// Форма создания
|
||||
$('#taskForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
if (!isDirector()) { location.hash = '#/home'; return; }
|
||||
const fd = new FormData(e.target);
|
||||
const tasks = getTasks();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
41
index.html
41
index.html
@ -11,7 +11,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
|
||||
<link rel="stylesheet" href="styles.css?v=30" />
|
||||
<link rel="stylesheet" href="styles.css?v=31" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@ -28,7 +28,7 @@
|
||||
<span class="menu-sub">Время, погода, новости отрасли</span>
|
||||
</span>
|
||||
</a>
|
||||
<a href="#/create" class="menu-item" data-view="create" role="menuitem">
|
||||
<a href="#/create" class="menu-item director-only" data-view="create" role="menuitem">
|
||||
<span class="menu-icon">+</span>
|
||||
<span class="menu-text">
|
||||
<span class="menu-title">Создать поручение</span>
|
||||
@ -54,12 +54,43 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="who">
|
||||
<label for="currentUser">Я работаю как</label>
|
||||
<select id="currentUser"></select>
|
||||
<div id="userChip" class="user-chip" hidden>
|
||||
<span class="user-chip-avatar" id="userChipAvatar">—</span>
|
||||
<span class="user-chip-text">
|
||||
<span class="user-chip-name" id="userChipName">—</span>
|
||||
<span class="user-chip-role" id="userChipRole">—</span>
|
||||
</span>
|
||||
<button id="logoutBtn" class="btn btn-ghost" type="button" title="Выйти">Выйти</button>
|
||||
</div>
|
||||
<button id="resetDemo" class="btn btn-ghost" title="Очистить данные и пересоздать демо">↺ Сброс демо</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Логин -->
|
||||
<div id="loginOverlay" class="login-overlay" hidden>
|
||||
<div class="login-card">
|
||||
<div class="login-brand">
|
||||
<img src="icon.jpg" alt="" class="login-logo" />
|
||||
<div>
|
||||
<div class="login-title">GR Department</div>
|
||||
<div class="login-sub">Помощник руководителя · ДВГО</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="login-h1">Вход</h1>
|
||||
<form id="loginForm" class="login-form">
|
||||
<label>Сотрудник
|
||||
<select id="loginUser" required></select>
|
||||
</label>
|
||||
<label id="loginPwdLabel" hidden>Пароль
|
||||
<input id="loginPwd" type="password" autocomplete="current-password" placeholder="Введите пароль" />
|
||||
</label>
|
||||
<div id="loginError" class="login-error" hidden>Неверный пароль.</div>
|
||||
<button type="submit" class="btn btn-primary">Войти</button>
|
||||
<p class="login-hint">Демо: пароль директора — <code>1234</code>. Остальные входят без пароля.</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
|
||||
<!-- Главная -->
|
||||
@ -231,6 +262,6 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/l10n/ru.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||
<script src="app.js?v=26"></script>
|
||||
<script src="app.js?v=27"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
95
styles.css
95
styles.css
@ -211,7 +211,100 @@ body {
|
||||
.brand:hover .brand-mark-secondary { opacity: 1; }
|
||||
|
||||
.who { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
body[data-view="home"] .who { display: none; }
|
||||
body[data-role="employee"] .director-only { display: none !important; }
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px 6px 6px;
|
||||
background: var(--surface-2);
|
||||
border-radius: var(--r-pill);
|
||||
}
|
||||
.user-chip-avatar {
|
||||
width: 30px; height: 30px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #4FB1F7, var(--kt-blue));
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.user-chip-text { display: flex; flex-direction: column; line-height: 1.15; }
|
||||
.user-chip-name { font-size: 13px; font-weight: 600; color: var(--ink); }
|
||||
.user-chip-role { font-size: 11px; color: var(--muted); }
|
||||
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, #F5F6F8 0%, #ECEFF4 100%);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
background: var(--surface);
|
||||
border-radius: 20px;
|
||||
padding: 36px 32px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.08), 0 4px 10px rgba(0,0,0,0.04);
|
||||
}
|
||||
.login-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.login-logo {
|
||||
width: 44px; height: 44px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.login-title { font-size: 15px; font-weight: 600; color: var(--ink); letter-spacing: -0.01em; }
|
||||
.login-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
.login-h1 {
|
||||
margin: 0 0 22px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.login-form { display: flex; flex-direction: column; gap: 14px; }
|
||||
.login-form label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--muted); font-weight: 500; }
|
||||
.login-form select, .login-form input {
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.login-form select:focus, .login-form input:focus {
|
||||
border-color: var(--kt-blue);
|
||||
box-shadow: 0 0 0 3px rgba(0,113,227,0.15);
|
||||
}
|
||||
.login-form .btn-primary {
|
||||
margin-top: 6px;
|
||||
padding: 12px 18px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.login-error {
|
||||
font-size: 13px;
|
||||
color: #FF453A;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 69, 58, 0.08);
|
||||
border-radius: var(--r-sm);
|
||||
}
|
||||
.login-hint { font-size: 12px; color: var(--muted); margin: 4px 0 0 0; }
|
||||
.login-hint code { background: var(--surface-2); padding: 1px 6px; border-radius: 4px; font-size: 12px; }
|
||||
.who label { color: var(--muted); font-size: 12px; font-weight: 400; }
|
||||
.who select {
|
||||
padding: 6px 28px 6px 12px;
|
||||
|
||||
Reference in New Issue
Block a user