Rename Director to Абделазиз, per-user passwords, profile modal
- Replace 'Директор' employee with 'Абделазиз' (director role). - Seed unique simple temporary passwords for every employee in localStorage on first run. Existing sessions/usernames migrated. - Login now requires a password for every employee. - Add personal cabinet (modal) reachable via the user chip with a change-password form (current + new + confirm, min 4 chars). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
46f21c2ee2
commit
f51d1f12c1
111
app.js
111
app.js
@ -3,7 +3,7 @@
|
|||||||
const STORAGE_KEY = 'sultan.tasks.v6';
|
const STORAGE_KEY = 'sultan.tasks.v6';
|
||||||
const USER_KEY = 'sultan.currentUser.v1';
|
const USER_KEY = 'sultan.currentUser.v1';
|
||||||
const SESSION_KEY = 'sultan.session.v1';
|
const SESSION_KEY = 'sultan.session.v1';
|
||||||
const DIRECTOR_PASSWORD = '1234';
|
const PASSWORDS_KEY = 'sultan.passwords.v1';
|
||||||
|
|
||||||
function getSession() {
|
function getSession() {
|
||||||
try {
|
try {
|
||||||
@ -13,7 +13,29 @@ function getSession() {
|
|||||||
}
|
}
|
||||||
function setSession(sess) { localStorage.setItem(SESSION_KEY, JSON.stringify(sess)); }
|
function setSession(sess) { localStorage.setItem(SESSION_KEY, JSON.stringify(sess)); }
|
||||||
function clearSession() { localStorage.removeItem(SESSION_KEY); }
|
function clearSession() { localStorage.removeItem(SESSION_KEY); }
|
||||||
function isDirector() { return state.currentUser === 'Директор'; }
|
function isDirector() { return state.currentUser === 'Абделазиз'; }
|
||||||
|
|
||||||
|
function loadPasswords() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(PASSWORDS_KEY);
|
||||||
|
const stored = raw ? JSON.parse(raw) : {};
|
||||||
|
let mutated = false;
|
||||||
|
EMPLOYEES.forEach(u => {
|
||||||
|
if (!stored[u]) { stored[u] = DEFAULT_PASSWORDS[u] || 'temp-1111'; mutated = true; }
|
||||||
|
});
|
||||||
|
if (mutated) localStorage.setItem(PASSWORDS_KEY, JSON.stringify(stored));
|
||||||
|
return stored;
|
||||||
|
} catch {
|
||||||
|
localStorage.setItem(PASSWORDS_KEY, JSON.stringify(DEFAULT_PASSWORDS));
|
||||||
|
return { ...DEFAULT_PASSWORDS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getPassword(user) { return loadPasswords()[user]; }
|
||||||
|
function setPassword(user, pwd) {
|
||||||
|
const all = loadPasswords();
|
||||||
|
all[user] = pwd;
|
||||||
|
localStorage.setItem(PASSWORDS_KEY, JSON.stringify(all));
|
||||||
|
}
|
||||||
|
|
||||||
// Очистка старых версий ключей (миграция).
|
// Очистка старых версий ключей (миграция).
|
||||||
(function purgeOldKeys() {
|
(function purgeOldKeys() {
|
||||||
@ -28,7 +50,7 @@ function isDirector() { return state.currentUser === 'Директор'; }
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const EMPLOYEES = [
|
const EMPLOYEES = [
|
||||||
'Директор',
|
'Абделазиз',
|
||||||
'Азат',
|
'Азат',
|
||||||
'Айгерим',
|
'Айгерим',
|
||||||
'Анар',
|
'Анар',
|
||||||
@ -39,6 +61,18 @@ const EMPLOYEES = [
|
|||||||
'Ислам',
|
'Ислам',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_PASSWORDS = {
|
||||||
|
'Абделазиз': 'aZ-9012',
|
||||||
|
'Азат': 'az-1122',
|
||||||
|
'Айгерим': 'ay-3344',
|
||||||
|
'Анар': 'an-5566',
|
||||||
|
'Арайлым': 'ar-7788',
|
||||||
|
'Гайнель': 'ga-2233',
|
||||||
|
'Гаухар': 'gh-4455',
|
||||||
|
'Елена': 'el-6677',
|
||||||
|
'Ислам': 'is-8899',
|
||||||
|
};
|
||||||
|
|
||||||
const DIRECTIONS = [
|
const DIRECTIONS = [
|
||||||
'ДВГО',
|
'ДВГО',
|
||||||
'НПА',
|
'НПА',
|
||||||
@ -135,7 +169,7 @@ function seedTasks() {
|
|||||||
title,
|
title,
|
||||||
description: '',
|
description: '',
|
||||||
assignee,
|
assignee,
|
||||||
created_by: 'Директор',
|
created_by: 'Абделазиз',
|
||||||
direction,
|
direction,
|
||||||
priority: computePriority(deadline),
|
priority: computePriority(deadline),
|
||||||
status: 'new',
|
status: 'new',
|
||||||
@ -218,7 +252,7 @@ function renderTable(tasks) {
|
|||||||
let visible = tasks.slice();
|
let visible = tasks.slice();
|
||||||
|
|
||||||
// Сотрудник видит только свои по умолчанию (если фильтр не задан явно).
|
// Сотрудник видит только свои по умолчанию (если фильтр не задан явно).
|
||||||
if (state.currentUser !== 'Директор' && !f.assignee) {
|
if (!isDirector() && !f.assignee) {
|
||||||
visible = visible.filter((t) => t.assignee === state.currentUser);
|
visible = visible.filter((t) => t.assignee === state.currentUser);
|
||||||
}
|
}
|
||||||
if (f.assignee) visible = visible.filter((t) => t.assignee === f.assignee);
|
if (f.assignee) visible = visible.filter((t) => t.assignee === f.assignee);
|
||||||
@ -838,26 +872,23 @@ function applyRoute() {
|
|||||||
// ---------- auth ----------
|
// ---------- auth ----------
|
||||||
|
|
||||||
function initAuth() {
|
function initAuth() {
|
||||||
const overlay = $('#loginOverlay');
|
|
||||||
const sel = $('#loginUser');
|
const sel = $('#loginUser');
|
||||||
const pwdLabel = $('#loginPwdLabel');
|
const pwdLabel = $('#loginPwdLabel');
|
||||||
const pwd = $('#loginPwd');
|
const pwd = $('#loginPwd');
|
||||||
const err = $('#loginError');
|
const err = $('#loginError');
|
||||||
const form = $('#loginForm');
|
const form = $('#loginForm');
|
||||||
|
|
||||||
|
loadPasswords();
|
||||||
|
|
||||||
fillSelect(sel, EMPLOYEES);
|
fillSelect(sel, EMPLOYEES);
|
||||||
sel.value = 'Директор';
|
sel.value = 'Абделазиз';
|
||||||
const togglePwd = () => {
|
pwdLabel.hidden = false;
|
||||||
pwdLabel.hidden = sel.value !== 'Директор';
|
sel.addEventListener('change', () => { pwd.value = ''; err.hidden = true; pwd.focus(); });
|
||||||
if (pwdLabel.hidden) { pwd.value = ''; err.hidden = true; }
|
|
||||||
};
|
|
||||||
togglePwd();
|
|
||||||
sel.addEventListener('change', togglePwd);
|
|
||||||
|
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const user = sel.value;
|
const user = sel.value;
|
||||||
if (user === 'Директор' && pwd.value !== DIRECTOR_PASSWORD) {
|
if (pwd.value !== getPassword(user)) {
|
||||||
err.hidden = false;
|
err.hidden = false;
|
||||||
pwd.focus();
|
pwd.focus();
|
||||||
return;
|
return;
|
||||||
@ -868,8 +899,19 @@ function initAuth() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#logoutBtn').addEventListener('click', logout);
|
$('#logoutBtn').addEventListener('click', logout);
|
||||||
|
$('#profileBtn').addEventListener('click', openProfile);
|
||||||
|
$('#profileClose').addEventListener('click', closeProfile);
|
||||||
|
$('#profileBackdrop').addEventListener('click', closeProfile);
|
||||||
|
$('#profileForm').addEventListener('submit', submitChangePassword);
|
||||||
|
|
||||||
|
// Миграция старой сессии "Директор" → "Абделазиз"
|
||||||
|
let sess = getSession();
|
||||||
|
if (sess && sess.user === 'Директор') {
|
||||||
|
sess = { ...sess, user: 'Абделазиз' };
|
||||||
|
setSession(sess);
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(USER_KEY) === 'Директор') localStorage.setItem(USER_KEY, 'Абделазиз');
|
||||||
|
|
||||||
const sess = getSession();
|
|
||||||
if (sess && sess.user && EMPLOYEES.includes(sess.user)) {
|
if (sess && sess.user && EMPLOYEES.includes(sess.user)) {
|
||||||
applySession(sess.user);
|
applySession(sess.user);
|
||||||
} else {
|
} else {
|
||||||
@ -877,6 +919,45 @@ function initAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openProfile() {
|
||||||
|
$('#profileName').textContent = state.currentUser || '—';
|
||||||
|
$('#profileRole').textContent = isDirector() ? 'Директор' : 'Сотрудник';
|
||||||
|
$('#profileForm').reset();
|
||||||
|
$('#profileError').hidden = true;
|
||||||
|
$('#profileSuccess').hidden = true;
|
||||||
|
$('#profileModal').hidden = false;
|
||||||
|
}
|
||||||
|
function closeProfile() { $('#profileModal').hidden = true; }
|
||||||
|
|
||||||
|
function submitChangePassword(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const cur = $('#pwdCurrent').value;
|
||||||
|
const next = $('#pwdNew').value;
|
||||||
|
const conf = $('#pwdConfirm').value;
|
||||||
|
const err = $('#profileError');
|
||||||
|
const ok = $('#profileSuccess');
|
||||||
|
ok.hidden = true;
|
||||||
|
err.hidden = true;
|
||||||
|
if (cur !== getPassword(state.currentUser)) {
|
||||||
|
err.textContent = 'Текущий пароль неверен.';
|
||||||
|
err.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!next || next.length < 4) {
|
||||||
|
err.textContent = 'Новый пароль слишком короткий (минимум 4 символа).';
|
||||||
|
err.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (next !== conf) {
|
||||||
|
err.textContent = 'Подтверждение не совпадает.';
|
||||||
|
err.hidden = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPassword(state.currentUser, next);
|
||||||
|
$('#profileForm').reset();
|
||||||
|
ok.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
function loginAs(user) {
|
function loginAs(user) {
|
||||||
setSession({ user, ts: Date.now() });
|
setSession({ user, ts: Date.now() });
|
||||||
applySession(user);
|
applySession(user);
|
||||||
|
|||||||
46
index.html
46
index.html
@ -11,7 +11,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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="https://cdn.jsdelivr.net/npm/flatpickr@4.6.13/dist/flatpickr.min.css">
|
||||||
<link rel="stylesheet" href="styles.css?v=31" />
|
<link rel="stylesheet" href="styles.css?v=32" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@ -55,17 +55,47 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="who">
|
<div class="who">
|
||||||
<div id="userChip" class="user-chip" hidden>
|
<div id="userChip" class="user-chip" hidden>
|
||||||
<span class="user-chip-avatar" id="userChipAvatar">—</span>
|
<button id="profileBtn" class="user-chip-main" type="button" title="Профиль">
|
||||||
<span class="user-chip-text">
|
<span class="user-chip-avatar" id="userChipAvatar">—</span>
|
||||||
<span class="user-chip-name" id="userChipName">—</span>
|
<span class="user-chip-text">
|
||||||
<span class="user-chip-role" id="userChipRole">—</span>
|
<span class="user-chip-name" id="userChipName">—</span>
|
||||||
</span>
|
<span class="user-chip-role" id="userChipRole">—</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button id="logoutBtn" class="btn btn-ghost" type="button" title="Выйти">Выйти</button>
|
<button id="logoutBtn" class="btn btn-ghost" type="button" title="Выйти">Выйти</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="resetDemo" class="btn btn-ghost" title="Очистить данные и пересоздать демо">↺ Сброс демо</button>
|
<button id="resetDemo" class="btn btn-ghost" title="Очистить данные и пересоздать демо">↺ Сброс демо</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Профиль / смена пароля -->
|
||||||
|
<div id="profileModal" class="modal" hidden>
|
||||||
|
<div class="modal-backdrop" id="profileBackdrop"></div>
|
||||||
|
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="profileTitle">
|
||||||
|
<button id="profileClose" class="modal-close" type="button" aria-label="Закрыть">×</button>
|
||||||
|
<h2 id="profileTitle" class="modal-title">Личный кабинет</h2>
|
||||||
|
<div class="profile-head">
|
||||||
|
<div class="profile-name" id="profileName">—</div>
|
||||||
|
<div class="profile-role" id="profileRole">—</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="modal-sub">Смена пароля</h3>
|
||||||
|
<form id="profileForm" class="login-form">
|
||||||
|
<label>Текущий пароль
|
||||||
|
<input id="pwdCurrent" type="password" autocomplete="current-password" required />
|
||||||
|
</label>
|
||||||
|
<label>Новый пароль
|
||||||
|
<input id="pwdNew" type="password" autocomplete="new-password" minlength="4" required />
|
||||||
|
</label>
|
||||||
|
<label>Подтвердите новый
|
||||||
|
<input id="pwdConfirm" type="password" autocomplete="new-password" minlength="4" required />
|
||||||
|
</label>
|
||||||
|
<div id="profileError" class="login-error" hidden></div>
|
||||||
|
<div id="profileSuccess" class="login-success" hidden>Пароль обновлён.</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Логин -->
|
<!-- Логин -->
|
||||||
<div id="loginOverlay" class="login-overlay" hidden>
|
<div id="loginOverlay" class="login-overlay" hidden>
|
||||||
<div class="login-card">
|
<div class="login-card">
|
||||||
@ -86,7 +116,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div id="loginError" class="login-error" hidden>Неверный пароль.</div>
|
<div id="loginError" class="login-error" hidden>Неверный пароль.</div>
|
||||||
<button type="submit" class="btn btn-primary">Войти</button>
|
<button type="submit" class="btn btn-primary">Войти</button>
|
||||||
<p class="login-hint">Демо: пароль директора — <code>1234</code>. Остальные входят без пароля.</p>
|
<p class="login-hint">Если забыли пароль — попросите Абделазиза сбросить через личный кабинет.</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,6 +292,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/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/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="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||||||
<script src="app.js?v=27"></script>
|
<script src="app.js?v=28"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
79
styles.css
79
styles.css
@ -215,11 +215,26 @@ body[data-role="employee"] .director-only { display: none !important; }
|
|||||||
.user-chip {
|
.user-chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 6px 8px 6px 6px;
|
padding: 4px 6px 4px 4px;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
border-radius: var(--r-pill);
|
border-radius: var(--r-pill);
|
||||||
}
|
}
|
||||||
|
.user-chip-main {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 2px 8px 2px 2px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
.user-chip-main:hover { background: rgba(0,113,227,0.08); }
|
||||||
|
.user-chip-main:focus-visible { outline: 2px solid var(--kt-blue); outline-offset: 2px; }
|
||||||
.user-chip-avatar {
|
.user-chip-avatar {
|
||||||
width: 30px; height: 30px;
|
width: 30px; height: 30px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -305,6 +320,66 @@ body[data-role="employee"] .director-only { display: none !important; }
|
|||||||
}
|
}
|
||||||
.login-hint { font-size: 12px; color: var(--muted); margin: 4px 0 0 0; }
|
.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; }
|
.login-hint code { background: var(--surface-2); padding: 1px 6px; border-radius: 4px; font-size: 12px; }
|
||||||
|
.login-success {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1B7F3A;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(48, 209, 88, 0.12);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Modal ---------- */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
.modal-card {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 440px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 30px 28px 28px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.18);
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 32px; height: 32px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.modal-close:hover { background: var(--surface-2); color: var(--ink); }
|
||||||
|
.modal-title { margin: 0 0 18px 0; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
||||||
|
.modal-sub { margin: 18px 0 12px 0; font-size: 14px; font-weight: 600; color: var(--ink-2); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.profile-head {
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.profile-name { font-size: 16px; font-weight: 600; color: var(--ink); }
|
||||||
|
.profile-role { font-size: 12px; color: var(--muted); padding: 4px 10px; background: var(--surface); border-radius: var(--r-pill); }
|
||||||
.who label { color: var(--muted); font-size: 12px; font-weight: 400; }
|
.who label { color: var(--muted); font-size: 12px; font-weight: 400; }
|
||||||
.who select {
|
.who select {
|
||||||
padding: 6px 28px 6px 12px;
|
padding: 6px 28px 6px 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user