Files
tech-service-portal/app/static/report.html
T

105 lines
8.8 KiB
HTML

<!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>