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 USER_KEY = 'sultan.currentUser.v1';
|
||||
const SESSION_KEY = 'sultan.session.v1';
|
||||
const DIRECTOR_PASSWORD = '1234';
|
||||
const PASSWORDS_KEY = 'sultan.passwords.v1';
|
||||
|
||||
function getSession() {
|
||||
try {
|
||||
@ -13,7 +13,29 @@ function getSession() {
|
||||
}
|
||||
function setSession(sess) { localStorage.setItem(SESSION_KEY, JSON.stringify(sess)); }
|
||||
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() {
|
||||
@ -28,7 +50,7 @@ function isDirector() { return state.currentUser === 'Директор'; }
|
||||
})();
|
||||
|
||||
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 = [
|
||||
'ДВГО',
|
||||
'НПА',
|
||||
@ -135,7 +169,7 @@ function seedTasks() {
|
||||
title,
|
||||
description: '',
|
||||
assignee,
|
||||
created_by: 'Директор',
|
||||
created_by: 'Абделазиз',
|
||||
direction,
|
||||
priority: computePriority(deadline),
|
||||
status: 'new',
|
||||
@ -218,7 +252,7 @@ function renderTable(tasks) {
|
||||
let visible = tasks.slice();
|
||||
|
||||
// Сотрудник видит только свои по умолчанию (если фильтр не задан явно).
|
||||
if (state.currentUser !== 'Директор' && !f.assignee) {
|
||||
if (!isDirector() && !f.assignee) {
|
||||
visible = visible.filter((t) => t.assignee === state.currentUser);
|
||||
}
|
||||
if (f.assignee) visible = visible.filter((t) => t.assignee === f.assignee);
|
||||
@ -838,26 +872,23 @@ function applyRoute() {
|
||||
// ---------- auth ----------
|
||||
|
||||
function initAuth() {
|
||||
const overlay = $('#loginOverlay');
|
||||
const sel = $('#loginUser');
|
||||
const pwdLabel = $('#loginPwdLabel');
|
||||
const pwd = $('#loginPwd');
|
||||
const err = $('#loginError');
|
||||
const form = $('#loginForm');
|
||||
|
||||
loadPasswords();
|
||||
|
||||
fillSelect(sel, EMPLOYEES);
|
||||
sel.value = 'Директор';
|
||||
const togglePwd = () => {
|
||||
pwdLabel.hidden = sel.value !== 'Директор';
|
||||
if (pwdLabel.hidden) { pwd.value = ''; err.hidden = true; }
|
||||
};
|
||||
togglePwd();
|
||||
sel.addEventListener('change', togglePwd);
|
||||
sel.value = 'Абделазиз';
|
||||
pwdLabel.hidden = false;
|
||||
sel.addEventListener('change', () => { pwd.value = ''; err.hidden = true; pwd.focus(); });
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const user = sel.value;
|
||||
if (user === 'Директор' && pwd.value !== DIRECTOR_PASSWORD) {
|
||||
if (pwd.value !== getPassword(user)) {
|
||||
err.hidden = false;
|
||||
pwd.focus();
|
||||
return;
|
||||
@ -868,8 +899,19 @@ function initAuth() {
|
||||
});
|
||||
|
||||
$('#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)) {
|
||||
applySession(sess.user);
|
||||
} 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) {
|
||||
setSession({ user, ts: Date.now() });
|
||||
applySession(user);
|
||||
|
||||
36
index.html
36
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=31" />
|
||||
<link rel="stylesheet" href="styles.css?v=32" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
@ -55,17 +55,47 @@
|
||||
</div>
|
||||
<div class="who">
|
||||
<div id="userChip" class="user-chip" hidden>
|
||||
<button id="profileBtn" class="user-chip-main" type="button" title="Профиль">
|
||||
<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>
|
||||
<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="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 class="login-card">
|
||||
@ -86,7 +116,7 @@
|
||||
</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>
|
||||
<p class="login-hint">Если забыли пароль — попросите Абделазиза сбросить через личный кабинет.</p>
|
||||
</form>
|
||||
</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/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=27"></script>
|
||||
<script src="app.js?v=28"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
79
styles.css
79
styles.css
@ -215,11 +215,26 @@ body[data-role="employee"] .director-only { display: none !important; }
|
||||
.user-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px 6px 6px;
|
||||
gap: 8px;
|
||||
padding: 4px 6px 4px 4px;
|
||||
background: var(--surface-2);
|
||||
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 {
|
||||
width: 30px; height: 30px;
|
||||
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 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 select {
|
||||
padding: 6px 28px 6px 12px;
|
||||
|
||||
Reference in New Issue
Block a user