Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android

This commit is contained in:
darkorion
2026-10-13 09:09:53 +03:00
commit c298f5a830
247 changed files with 23200 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
"use strict";
// ---------- Обратная связь по порталу ----------
// Неприметная ссылка внизу каждой страницы открывает короткую форму. Ответы видят только администраторы портала (#/feedback).
const FB_KIND = { idea: "💡 Идея", bug: "🐞 Ошибка", other: "💬 Другое" };
const FB_STATUS = { new: "Новое", taken: "В работе", done: "Сделано", rejected: "Не будем делать" };
function feedbackFooter() {
const foot = document.getElementById("appfoot");
if (!foot) return;
foot.replaceChildren(me ? h("a", { href: "javascript:void 0", class: "fblink", onclick: openFeedback }, "Предложить улучшение портала или сообщить об ошибке") : "");
}
function openFeedback() {
if (document.querySelector(".fbmodal")) return;
const st = { kind: "idea", text: "" };
const kinds = h("div", { class: "row" });
const drawKinds = () => kinds.replaceChildren(...Object.entries(FB_KIND).map(([k, t]) =>
h("button", { type: "button", class: "chipf" + (st.kind === k ? " on" : ""), onclick: () => { st.kind = k; drawKinds(); } }, t)));
drawKinds();
const text = h("textarea", { rows: 5, maxlength: 4000, placeholder: "Что улучшить или что не работает? Где именно, как повторить." });
const close = () => wrap.remove();
const wrap = h("div", { class: "fbmodal", onclick: e => { if (e.target === wrap) close(); } },
h("div", { class: "card" }, h("h2", { style: "margin-top:0" }, "Обратная связь по порталу"),
h("p", { class: "small muted" }, "Сообщение получат администраторы портала. Раздел, где вы сейчас, подставится сам."),
kinds, h("div", { style: "margin-top:8px" }, text),
h("div", { class: "row", style: "margin-top:10px" },
h("button", { onclick: guard(async () => {
if (text.value.trim().length < 3) throw new Error("Опишите идею или проблему");
await api("POST", "/feedback", { kind: st.kind, text: text.value, page: (location.hash || "#/").slice(0, 200), client: "web" });
close(); toast("Спасибо! Сообщение отправлено");
}) }, "Отправить"), h("button", { class: "sec", onclick: close }, "Отмена"))));
document.body.append(wrap);
text.focus();
}
async function feedbackPage(which) {
if (!me.portal_admin) throw new Error("Доступно только администраторам портала");
const status = which === "all" ? "" : which === "done" ? "done" : "open";
const list = await api("GET", "/feedback" + (status ? "?status=" + status : ""));
const card = f => {
const sel = h("select", {}, ...Object.entries(FB_STATUS).map(([k, t]) => h("option", { value: k, selected: f.status === k }, t)));
const ans = h("textarea", { rows: 2, placeholder: "Заметка: что решили, что сделано" }, f.answer);
return h("div", { class: "card fbcard fb-" + f.status },
h("div", { class: "row" }, h("b", { style: "flex:1" }, FB_KIND[f.kind] || f.kind), h("span", { class: "badge" }, FB_STATUS[f.status] || f.status)),
h("div", { class: "small muted" }, [fmt(f.created_at), f.user_name || "—", f.page ? "раздел " + f.page : "", { web: "сайт", m: "мобильная версия", android: "приложение" }[f.client] || f.client].filter(Boolean).join(" · ")),
h("div", { style: "margin-top:6px;white-space:pre-wrap" }, f.text),
f.answered_by_name ? h("div", { class: "small muted", style: "margin-top:4px" }, `Обновил ${f.answered_by_name}, ${fmt(f.answered_at)}`) : null,
h("div", { class: "grid", style: "margin-top:8px" }, field("Статус", sel), field("Заметка", ans)),
h("div", { class: "row", style: "margin-top:8px" },
h("button", { class: "sm", onclick: guard(async () => { await api("PATCH", `/feedback/${f.id}`, { status: sel.value, answer: ans.value }); toast("Сохранено"); feedbackPage(which); }) }, "Сохранить"),
h("button", { class: "sec sm", onclick: guard(async () => { if (confirm("Удалить сообщение?")) { await api("DELETE", `/feedback/${f.id}`); feedbackPage(which); } }) }, "Удалить")));
};
fill($app, h("h1", {}, "Обратная связь по порталу"),
tabs([["#/feedback", "Открытые"], ["#/feedback/done", "Сделано"], ["#/feedback/all", "Все"]], which === "all" ? "#/feedback/all" : which === "done" ? "#/feedback/done" : "#/feedback"),
h("p", { class: "small muted" }, "Идеи и замечания сотрудников. Видят только администраторы портала. Ссылка на форму — внизу каждой страницы и в меню приложения."),
...(list.length ? list.map(card) : [h("div", { class: "card muted" }, "Сообщений нет")]));
}