Портал технической службы: наряды и чек-листы, задачи, отказы и неисправности, анализ надёжности, карты, склады, приложение для Android
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Сообщить о неисправности</title>
|
||||
<style>
|
||||
:root{--bg:#f4f5f8;--card:#fff;--text:#1d2330;--muted:#6b7385;--line:#dfe3ea;--accent:#2563eb;--bad:#c62828;--ok:#16803c}
|
||||
@media (prefers-color-scheme:dark){:root{--bg:#12151c;--card:#1b2029;--text:#e6e9f0;--muted:#98a1b3;--line:#2c3340;--accent:#6c9bff;--bad:#f87171;--ok:#4ade80}}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:var(--bg);color:var(--text);font:16px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
||||
main{max-width:560px;margin:0 auto;padding:24px 16px 40px}
|
||||
h1{font-size:24px;margin:0 0 4px}
|
||||
.muted{color:var(--muted)}.small{font-size:13px}
|
||||
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px;margin-top:16px}
|
||||
label{display:block;font-size:14px;font-weight:600;margin:14px 0 6px}
|
||||
label:first-child{margin-top:0}
|
||||
label span{color:var(--bad)}
|
||||
input,select,textarea{width:100%;font:inherit;padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--text);min-height:48px}
|
||||
textarea{min-height:120px;resize:vertical}
|
||||
select:disabled{opacity:.55}
|
||||
.opts{display:flex;flex-direction:column;gap:6px}.opt{display:flex;gap:8px;align-items:center;font-weight:400;margin:0}
|
||||
.hint{font-size:13px;color:var(--muted);margin-top:4px}
|
||||
button{font:inherit;font-weight:600;min-height:52px;width:100%;margin-top:18px;border:0;border-radius:12px;background:#c62828;color:#fff;cursor:pointer}
|
||||
button:disabled{opacity:.5}
|
||||
.err{color:var(--bad);margin-top:12px}
|
||||
.done{text-align:center}.done b{font-size:44px;display:block;color:var(--ok)}
|
||||
.trap{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="app"><p class="muted">Загрузка…</p></main>
|
||||
<script>
|
||||
"use strict";
|
||||
const $ = document.getElementById("app");
|
||||
const h = (t, a, ...k) => { const e = document.createElement(t); for (const [n, v] of Object.entries(a || {})) { if (v == null || v === false) continue; if (n.startsWith("on")) e.addEventListener(n.slice(2), v); else if (n === "class") e.className = v; else if (v === true) e.setAttribute(n, ""); else e.setAttribute(n, v); } for (const c of k.flat()) if (c != null && c !== false) e.append(c.nodeType ? c : document.createTextNode(c)); return e; };
|
||||
|
||||
async function start() {
|
||||
let o;
|
||||
try { o = await (await fetch("/api/v1/public/report-options")).json(); }
|
||||
catch (e) { $.replaceChildren(h("p", { class: "err" }, "Нет связи с сервером. Попробуйте позже.")); return; }
|
||||
if (!o.enabled) { $.replaceChildren(h("h1", {}, "Сообщить о неисправности"), h("p", { class: "muted" }, "Приём обращений через эту форму сейчас отключён. Свяжитесь с технической службой по телефону.")); return; }
|
||||
|
||||
// место выбирается из дерева: показываем путь «Цех №1 / Компрессорная»
|
||||
const byId = Object.fromEntries(o.locations.map(l => [l.id, l]));
|
||||
const path = l => { const p = []; let x = l, guard = 0; while (x && guard++ < 20) { p.unshift(x.name); x = byId[x.parent_id]; } return p.join(" / "); };
|
||||
const under = id => { const out = new Set([id]); let grown = true; while (grown) { grown = false; for (const l of o.locations) if (!out.has(l.id) && out.has(l.parent_id)) { out.add(l.id); grown = true; } } return out; };
|
||||
const locs = o.locations.map(l => ({ id: l.id, label: path(l) })).filter(l => o.equipment.some(e => under(l.id).has(e.location_id))).sort((a, b) => a.label.localeCompare(b.label, "ru"));
|
||||
|
||||
const loc = h("select", { required: true }, h("option", { value: "" }, "— выберите место —"), ...locs.map(l => h("option", { value: l.id }, l.label)));
|
||||
const eq = h("select", { required: true, disabled: true }, h("option", { value: "" }, "— сначала выберите место —"));
|
||||
const desc = h("textarea", { required: true, maxlength: 4000, placeholder: "Что случилось: шум, течь, не включается, искрит… Когда заметили?" });
|
||||
const name = h("input", { required: true, maxlength: 150, autocomplete: "name", placeholder: "Иванов Иван" });
|
||||
const contact = h("input", { required: true, maxlength: 200, autocomplete: "tel", placeholder: "+7 900 000-00-00 или почта" });
|
||||
const trap = h("input", { tabindex: "-1", autocomplete: "off" });
|
||||
// оборудование остановлено — инцидент (авария); работает, но есть проблема — сообщение о неисправности
|
||||
const kindRadio = (v, t, on) => h("label", { class: "opt" }, h("input", { type: "radio", name: "kind", value: v, checked: on }), t);
|
||||
const kinds = h("div", { class: "opts" }, kindRadio("fault", "Работает, но есть неисправность", true), kindRadio("incident", "Остановлено, авария"));
|
||||
const kind = () => (kinds.querySelector("input:checked") || {}).value || "fault";
|
||||
const err = h("div", { class: "err" }), btn = h("button", { type: "submit" }, "Отправить сообщение");
|
||||
|
||||
loc.onchange = () => {
|
||||
const set = loc.value ? under(+loc.value) : new Set();
|
||||
const list = o.equipment.filter(e => set.has(e.location_id));
|
||||
eq.replaceChildren(h("option", { value: "" }, list.length ? "— выберите машину —" : "— в этом месте нет оборудования —"),
|
||||
...list.map(e => h("option", { value: e.id }, e.name + (e.inventory_no ? ` (${e.inventory_no})` : ""))));
|
||||
eq.disabled = !list.length;
|
||||
};
|
||||
|
||||
const form = h("form", { class: "card", novalidate: true, onsubmit: async ev => {
|
||||
ev.preventDefault(); err.textContent = "";
|
||||
if (!loc.value) return fail("Выберите место", loc);
|
||||
if (!eq.value) return fail("Выберите машину", eq);
|
||||
if (desc.value.trim().length < 5) return fail("Опишите, что произошло", desc);
|
||||
if (name.value.trim().length < 2) return fail("Укажите, как к вам обращаться", name);
|
||||
if (contact.value.trim().length < 5) return fail("Укажите телефон или почту для связи", contact);
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await fetch("/api/v1/public/incidents", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
|
||||
location_id: +loc.value, equipment_id: +eq.value, kind: kind(), description: desc.value, reporter_name: name.value, reporter_contact: contact.value, website: trap.value }) });
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(typeof d.detail === "string" ? d.detail : "Проверьте заполнение полей");
|
||||
$.replaceChildren(h("div", { class: "card done" }, h("b", {}, "✓"), h("h1", {}, "Сообщение отправлено"),
|
||||
h("p", {}, d.number ? `Номер обращения: ${d.number}.` : "", " Техническая служба уже получила уведомление и свяжется с вами при необходимости."),
|
||||
h("p", {}, h("a", { href: "/report" }, "Сообщить ещё об одной неисправности"))));
|
||||
} catch (e) { btn.disabled = false; err.textContent = e instanceof TypeError ? "Нет связи с сервером. Попробуйте ещё раз." : e.message; }
|
||||
} },
|
||||
h("label", {}, "Место ", h("span", {}, "*")), loc, h("div", { class: "hint" }, "Список машин ниже покажет только то, что стоит в этом месте."),
|
||||
h("label", {}, "Машина ", h("span", {}, "*")), eq,
|
||||
h("label", {}, "Оборудование ", h("span", {}, "*")), kinds,
|
||||
h("label", {}, "Что произошло ", h("span", {}, "*")), desc,
|
||||
h("label", {}, "Ваше имя ", h("span", {}, "*")), name,
|
||||
h("label", {}, "Телефон или почта ", h("span", {}, "*")), contact,
|
||||
h("div", { class: "trap", "aria-hidden": "true" }, h("label", {}, "Сайт"), trap),
|
||||
err, btn);
|
||||
function fail(msg, el) { err.textContent = msg; el.focus(); }
|
||||
$.replaceChildren(h("h1", {}, "Сообщить о неисправности"),
|
||||
h("p", { class: "muted" }, "Заметили аварию, течь, посторонний шум или другую проблему с оборудованием? Заполните форму — техническая служба получит сообщение сразу."), form,
|
||||
h("p", { class: "small muted", style: "margin-top:14px" }, "Контакты нужны только для уточнения деталей и видны сотрудникам технической службы."));
|
||||
}
|
||||
start();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user