feat: «глазик» в поле пароля при входе и смена своего пароля (сайт — своя карточка, приложение — «Настройки»); Android 1.11.0

This commit is contained in:
darkorion
2026-10-16 16:41:52 +03:00
parent 4f2aed1770
commit 4cebf300aa
17 changed files with 184 additions and 12 deletions
+1
View File
@@ -45,6 +45,7 @@
- Не вижу наряд: наряд на должность виден только сотрудникам этой должности, которые сейчас на смене по своему графику; проверьте график в своей карточке.
- График работы задаётся сотруднику (#/users → «Изменить» → «График работы»), не должности. Одну должность могут занимать сменщики: наряды и уведомления должности получает только тот, кто на смене. Без графика — всегда на смене.
- Фото к пункту чек-листа или к заданию можно отметить «хранить более года» (в конструкторе чек-листа рядом с режимом фото, в форме задания) — такие фото не удаляются автоочисткой. Автоочистку (удалять фото старше N дней) включает администратор портала в #/server.
- Смена своего пароля: своя карточка (имя вверху меню, #/user/<свой id>) → блок «🔑 Пароль» → «Сменить пароль»; в приложении Android — «Настройки» → «Сменить пароль». Нужен текущий пароль; другие устройства выйдут из системы. Забытый пароль сбрасывает администратор или модератор в #/users. При входе значок 👁 показывает введённый пароль.
- Мониторинг сервера — #/monitor (администратор портала): загрузка процессора, память, место на дисках.
- Фильтр «Место» над списком оборудования #/equipment и кранов: показывает машины места и вложенных мест.
- Мастер первичной настройки: «Завершить и проверить» открывает проверку готовности #/setup/done — что мешает работе и где поправить.
+14 -1
View File
@@ -180,6 +180,19 @@ async function route() {
}
window.addEventListener("hashchange", route);
// поле пароля с «глазиком»: показать или скрыть введённое
function pwEye(input) {
const btn = h("button", { type: "button", class: "pweye", title: "Показать пароль", "aria-label": "Показать пароль" }, "👁");
btn.onclick = () => {
const show = input.type === "password";
input.type = show ? "text" : "password";
btn.textContent = show ? "🙈" : "👁";
btn.title = show ? "Скрыть пароль" : "Показать пароль";
btn.setAttribute("aria-label", btn.title);
};
return h("div", { class: "pwwrap" }, input, btn);
}
function loginPage() {
$top.replaceChildren(); $banner.replaceChildren(); document.getElementById("appfoot")?.replaceChildren();
const u = h("input", { autofocus: true, autocomplete: "username" }), p = h("input", { type: "password", autocomplete: "current-password" });
@@ -205,7 +218,7 @@ function loginPage() {
}).catch(() => {});
fetch(API + "/demo/status").then(r => r.json()).then(d => { if (d.active) note.style.display = ""; }).catch(() => {});
$app.replaceChildren(h("div", { class: "card login" }, h("h1", {}, "Вход"), note,
h("label", {}, "Логин"), u, h("div", { style: "height:10px" }), h("label", {}, "Пароль"), p,
h("label", {}, "Логин"), u, h("div", { style: "height:10px" }), h("label", {}, "Пароль"), pwEye(p),
h("div", { style: "height:14px" }), h("button", { onclick: go }, "Войти"), h("div", { style: "margin-top:14px" }, apk), docs));
}
+4
View File
@@ -81,3 +81,7 @@ body.menu-open #shade{display:block;position:fixed;inset:0;background:rgba(0,0,0
.fbm{opacity:.7;font-size:13px}
.fbmodal{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:200;padding:14px}
.fbmodal .card{width:100%;max-width:520px}.fbmodal textarea{width:100%;margin-top:8px}
/* поле пароля с «глазиком» */
.pwwrap{position:relative}.pwwrap input{width:100%;padding-right:44px}
.pweye{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:transparent;border:0;padding:4px 8px;font-size:18px;cursor:pointer;color:inherit;width:auto;margin:0}
+15 -2
View File
@@ -399,7 +399,7 @@ function renderDrawer() {
h("a", { href: "#", class: route.page === "list" ? "on" : "", onclick: e => { e.preventDefault(); go("")(); } }, h("i", {}, "📋"), "Наряды"),
h("a", { href: "#/s", class: route.page === "settings" ? "on" : "", onclick: e => { e.preventDefault(); go("#/s")(); } }, h("i", {}, "⚙"), "Настройки и синхронизация"),
h("div", { class: "navh" }, "Портал (нужна связь)"),
web("#/work", "🎯", "Задачи: всё в одном месте"), web("#/tasks", "🗓", "Задачи и поручения"), web("#/equipment", "🔧", "Оборудование и инциденты"), web("#/faults", "⚠", "Неисправности"), web("#/map", "🗺", "Карта завода"), me.crane_map ? web("#/cranes", "🏗", "Краны") : null, me.crane_map ? web("#/map/cranes", "🗺", "Карта кранов") : null,
web("#/work", "🎯", "Задачи: всё в одном месте"), web("#/tasks", "🗓", "Задачи и поручения"), web("#/equipment", "🔧", "Оборудование и отказы"), web("#/faults", "⚠", "Неисправности"), web("#/map", "🗺", "Карта завода"), me.crane_map ? web("#/cranes", "🏗", "Краны") : null, me.crane_map ? web("#/map/cranes", "🗺", "Карта кранов") : null,
web("#/my-tools", "🧰", "Мой инструмент"), web("#/requests", "🛒", "Заявки на инструмент"),
store || eng ? web("#/inventory", "🏬", "Склады и учёт") : null,
web("#/people", "👥", "Сотрудники"), web("#/user/" + me.id, "👤", "Моя карточка"),
@@ -448,6 +448,19 @@ const badges = o => h("span", { class: "row", style: "gap:6px" }, h("span", { cl
const progress = o => { const t = (o.items || []).length; return t ? [(o.items || []).filter(itemDone).length, t] : null; };
// ----- вход -----
// поле пароля с «глазиком»: показать или скрыть введённое
function pwEye(input) {
const btn = h("button", { type: "button", class: "pweye", title: "Показать пароль", "aria-label": "Показать пароль" }, "👁");
btn.onclick = () => {
const show = input.type === "password";
input.type = show ? "text" : "password";
btn.textContent = show ? "🙈" : "👁";
btn.title = show ? "Скрыть пароль" : "Показать пароль";
btn.setAttribute("aria-label", btn.title);
};
return h("div", { class: "pwwrap" }, input, btn);
}
function loginView(my) {
setBar(h("h1", {}, "Чек-листы"));
const u = h("input", { autocomplete: "username", autocapitalize: "none", autocorrect: "off", placeholder: "Логин" }), p = h("input", { type: "password", autocomplete: "current-password", placeholder: "Пароль" });
@@ -472,7 +485,7 @@ function loginView(my) {
const ios = /iPhone|iPad|iPod/i.test(navigator.userAgent) && !navigator.standalone;
commit(my, h("div", { class: "login stack" }, h("h1", {}, "🔧 Чек-листы"), h("p", { class: "muted" }, "Обходы и обслуживание оборудования"),
sync.error && hasSession() === false && sync.error.startsWith("Сессия") ? h("p", { class: "warn" }, sync.error) : null,
u, p, err, btn, apk,
u, pwEye(p), err, btn, apk,
ios ? h("p", { class: "small muted" }, "Чтобы установить: нажмите «Поделиться» → «На экран Домой».") : null,
h("p", { class: "small" }, h("a", { href: "/", onclick: () => localStorage.setItem("stay_desktop", "1") }, "Полная версия сайта"))));
}
+24
View File
@@ -808,6 +808,29 @@ async function peoplePage() {
draw();
}
// Смена своего пароля: старый, новый и повтор. Остальные сессии завершаются, эта получает новые токены.
function passwordCard() {
const inp = ac => h("input", { type: "password", autocomplete: ac });
const old = inp("current-password"), nw = inp("new-password"), rep = inp("new-password");
const body = h("div", { style: "display:none;margin-top:8px" },
h("div", { class: "grid" }, field("Текущий пароль", pwEye(old)), field("Новый пароль", pwEye(nw), "Не короче 10 символов"), field("Новый пароль ещё раз", pwEye(rep))),
h("div", { class: "row", style: "margin-top:10px" },
h("button", { onclick: guard(async () => {
if (!old.value) throw new Error("Введите текущий пароль");
if (nw.value.length < 10) throw new Error("Новый пароль — не короче 10 символов");
if (nw.value !== rep.value) throw new Error("Новые пароли не совпадают");
if (nw.value === old.value) throw new Error("Новый пароль совпадает с текущим");
const t = await api("POST", "/auth/change-password", { old_password: old.value, new_password: nw.value });
token = t.access_token; localStorage.setItem("token", token); localStorage.setItem("refresh", t.refresh_token);
old.value = nw.value = rep.value = ""; body.style.display = "none";
toast("Пароль изменён. На других устройствах войдите заново");
}) }, "Сменить пароль"),
h("button", { class: "sec", onclick: () => { body.style.display = "none"; } }, "Отмена")));
return h("div", { class: "card" }, h("div", { class: "row" }, h("h2", { style: "margin:0;flex:1" }, "🔑 Пароль"),
h("button", { class: "sec sm", onclick: () => { body.style.display = body.style.display === "none" ? "" : "none"; if (body.style.display === "") old.focus(); } }, "Сменить пароль")),
h("div", { class: "small muted", style: "margin-top:4px" }, "После смены пароля вход на других устройствах и в приложении нужно будет выполнить заново."), body);
}
async function userCardPage(id) {
const c = await api("GET", `/users/${id}/card`);
const u = c.user, reload = () => userCardPage(id);
@@ -824,6 +847,7 @@ async function userCardPage(id) {
u.department ? h("div", { style: "margin-top:2px" }, "🏢 ", h("a", { href: "#/people?dep=" + u.department.id }, u.department.name)) : null,
u.position ? h("div", { class: "small", style: "margin-top:4px" }, u.on_shift ? "🟢 Сейчас на смене" : "⚪ Сейчас не на смене") : null,
u.phone && !c.can_edit ? h("div", { style: "margin-top:6px" }, "📞 ", h("a", { href: "tel:" + u.phone.replace(/[^+\d]/g, "") }, u.phone)) : null)),
+id === me.id ? passwordCard() : null,
h("div", { class: "cols" },
h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, "График"),
c.schedule.name ? h("div", { class: "small muted", style: "margin-bottom:6px" }, c.schedule.name) : null,
+4
View File
@@ -306,3 +306,7 @@ button.fault{background:var(--fault);border-color:var(--fault);color:#fff}
/* кнопка закрытия отказа и неисправности: крупная и заметная */
button.closebtn{background:#16a34a;border:1px solid #16a34a;color:#fff;font-size:16px;font-weight:700;padding:10px 20px;border-radius:10px}
button.closebtn:hover{filter:brightness(1.08)}
/* поле пароля с «глазиком» */
.pwwrap{position:relative}.pwwrap input{width:100%;padding-right:44px}
.pweye{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:transparent;border:0;padding:4px 8px;font-size:18px;cursor:pointer;color:inherit;width:auto;margin:0}