592 lines
22 KiB
JavaScript
592 lines
22 KiB
JavaScript
"use strict";
|
||
/* Журнал заявок на ремонт оборудования
|
||
Архетип: операционная очередь (queue). Данные — localStorage. */
|
||
|
||
var STORAGE_KEY = "remont-zayavok-v1";
|
||
|
||
var STATUSES = {
|
||
new: { label: "Новая", tone: "info" },
|
||
inwork: { label: "В работе", tone: "warn" },
|
||
waiting: { label: "Ожидает запчасти", tone: "muted" },
|
||
done: { label: "Выполнено", tone: "ok" },
|
||
cancelled: { label: "Отменена", tone: "risk" }
|
||
};
|
||
|
||
var PRIORITIES = {
|
||
low: { label: "Низкий" },
|
||
normal: { label: "Средний", tone: "muted" },
|
||
high: { label: "Высокий", tone: "risk" }
|
||
};
|
||
|
||
var PRIORITY_OPTIONS = [
|
||
{ key: "", label: "Все" },
|
||
{ key: "high", label: "Высокий" },
|
||
{ key: "normal", label: "Средний" },
|
||
{ key: "low", label: "Низкий" }
|
||
];
|
||
|
||
function todayIso(offsetDays) {
|
||
var d = new Date();
|
||
d.setDate(d.getDate() + (offsetDays || 0));
|
||
var m = String(d.getMonth() + 1);
|
||
var day = String(d.getDate());
|
||
if (m.length < 2) m = "0" + m;
|
||
if (day.length < 2) day = "0" + day;
|
||
return d.getFullYear() + "-" + m + "-" + day;
|
||
}
|
||
|
||
function seedRequests() {
|
||
return [
|
||
{ id: "ОБ-2026-0027", equipment: "Коммутатор C803", address: "Машинный зал, стеллаж 4, стойка 02", reporter: "Айбек С.", created: todayIso(-1), status: "new", priority: "high", reason: "Оборудование не отвечает, индикатор питания не горит" },
|
||
{ id: "ОБ-2026-0026", equipment: "UPS 3 кВА", address: "Машинный зал, стеллаж 3", reporter: "Айбек С.", created: todayIso(-1), status: "inwork", priority: "normal", reason: "Звуковое оповещение о разряде батареи не отключается" },
|
||
{ id: "ОБ-2026-0025", equipment: "Кабельная линейка UTP, 6-я линия", address: "Служебный модуль, линия 06", reporter: "Данияр К.", created: todayIso(-2), status: "waiting", priority: "normal", reason: "Постоянные обрывы линии, обрыв фиксируется каждые 10–15 минут" },
|
||
{ id: "ОБ-2026-0024", equipment: "Маршрутизатор", address: "Уровень 4, шкаф связи", reporter: "Сейсем М.", created: todayIso(-4), status: "inwork", priority: "high", reason: "Нагрузка на устройства уровня 4 нестабильна, скорость падает" },
|
||
{ id: "ОБ-2026-0023", equipment: "Коммутатор ЦОД", address: "Машинный зал, стеллаж 3, стойка 01", reporter: "Айбек С.", created: todayIso(-5), status: "done", priority: "normal", reason: "Появились ошибки в портах, пакетная потеря" },
|
||
{ id: "ОБ-2026-0022", equipment: "Серверный шкаф, дверь", address: "Служебный модуль, линия 14", reporter: "Данияр К.", created: todayIso(-6), status: "done", priority: "low", reason: "Блокиратор двери не фиксирует, закрывает с задержкой" },
|
||
{ id: "ОБ-2026-0021", equipment: "Телевизор, зал ожидания", address: "Этаж 2, зона ожидания", reporter: "Сейсем М.", created: todayIso(-7), status: "done", priority: "low", reason: "Нет изображения, звук работает" },
|
||
{ id: "ОБ-2026-0020", equipment: "Телевизор, зона ожидания", address: "Этаж 2, зона ожидания", reporter: "Эльмира Б.", created: todayIso(-9), status: "done", priority: "low", reason: "Изображение мерцает" },
|
||
{ id: "ОБ-2026-0019", equipment: "Устройство резервного питания", address: "Служебный модуль, линия 06", reporter: "Данияр К.", created: todayIso(-10), status: "waiting", priority: "normal", reason: "Батарея держит заряд 3 минуты, требует замены" },
|
||
{ id: "ОБ-2026-0018", equipment: "Кабельная линейка, 3-я линия", address: "Служебный модуль, линия 03", reporter: "Айбек С.", created: todayIso(-12), status: "cancelled", priority: "normal", reason: "Обрыв линии — по заявке, дубликат" }
|
||
];
|
||
}
|
||
|
||
function nextId() {
|
||
var n = (window.REMOUNT_NEXT || 0);
|
||
n += 1;
|
||
window.REMOUNT_NEXT = n;
|
||
var s = String(n);
|
||
while (s.length < 4) s = "0" + s;
|
||
return "ОБ-2026-" + s;
|
||
}
|
||
|
||
/* ---------- Состояние приложения ---------- */
|
||
var state = {
|
||
requests: [],
|
||
query: "",
|
||
statusFilter: "", // "" = все
|
||
priorityFilter: "", // "" = все
|
||
openId: null, // id записи в drawer
|
||
drawerMode: "" // "" | "view" | "new" | "edit"
|
||
};
|
||
|
||
function loadRequests() {
|
||
var raw = "";
|
||
try {
|
||
raw = localStorage.getItem(STORAGE_KEY);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
if (!raw) return null;
|
||
var parsed = null;
|
||
try {
|
||
parsed = JSON.parse(raw);
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
if (!parsed || !parsed.requests) return null;
|
||
if (typeof parsed.nextSeq === "number") window.REMOUNT_NEXT = parsed.nextSeq;
|
||
return parsed.requests;
|
||
}
|
||
|
||
function saveRequests() {
|
||
var data = { requests: state.requests, nextSeq: window.REMOUNT_NEXT || 0 };
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||
} catch (e) {
|
||
/* localStorage может быть недоступен — просто не сохраняем */
|
||
}
|
||
}
|
||
|
||
function initRequests() {
|
||
var loaded = loadRequests();
|
||
if (loaded && loaded.length > 0) {
|
||
state.requests = loaded;
|
||
} else {
|
||
state.requests = seedRequests();
|
||
var maxSeq = 27;
|
||
for (var i = 0; i < state.requests.length; i++) {
|
||
var m = /(\d+)$/.exec(state.requests[i].id);
|
||
if (m) {
|
||
var num = parseInt(m[1], 10);
|
||
if (num > maxSeq) maxSeq = num;
|
||
}
|
||
}
|
||
window.REMOUNT_NEXT = maxSeq;
|
||
saveRequests();
|
||
}
|
||
}
|
||
|
||
/* ---------- Утилиты ---------- */
|
||
function $(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||
});
|
||
}
|
||
|
||
function fmtDate(iso) {
|
||
if (!iso) return "—";
|
||
var p = iso.split("-");
|
||
if (p.length !== 3) return iso;
|
||
return p[2] + "." + p[1] + "." + p[0];
|
||
}
|
||
|
||
function findRequest(id) {
|
||
for (var i = 0; i < state.requests.length; i++) {
|
||
if (state.requests[i].id === id) return state.requests[i];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function statusPill(status) {
|
||
var st = STATUSES[status] || { label: status, tone: "muted" };
|
||
return '<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span>";
|
||
}
|
||
|
||
function prioChip(p) {
|
||
var pr = PRIORITIES[p] || PRIORITIES.normal;
|
||
return '<span class="kt-ai-chip" data-tone="' + pr.tone + '">' + esc(pr.label) + "</span>";
|
||
}
|
||
|
||
function filteredRequests(q) {
|
||
q = (q == null ? "" : q).trim().toLowerCase();
|
||
var out = [];
|
||
for (var i = 0; i < state.requests.length; i++) {
|
||
var r = state.requests[i];
|
||
if (state.statusFilter && r.status !== state.statusFilter) continue;
|
||
if (state.priorityFilter && r.priority !== state.priorityFilter) continue;
|
||
if (q) {
|
||
if (String(r.equipment).toLowerCase().indexOf(q) === -1) continue;
|
||
}
|
||
out.push(r);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function statusCounts(q) {
|
||
var c = { "": 0, new: 0, inwork: 0, waiting: 0, done: 0, cancelled: 0 };
|
||
for (var i = 0; i < state.requests.length; i++) {
|
||
var r = state.requests[i];
|
||
if (state.priorityFilter && r.priority !== state.priorityFilter) continue;
|
||
if (q) {
|
||
if (String(r.equipment).toLowerCase().indexOf(q) === -1) continue;
|
||
}
|
||
c[""] += 1;
|
||
c[r.status] = (c[r.status] || 0) + 1;
|
||
}
|
||
return c;
|
||
}
|
||
|
||
/* ---------- Отрисовка ---------- */
|
||
function renderKpis() {
|
||
var c = statusCounts(state.query);
|
||
var items = [
|
||
{ value: c[""], label: "Заявки всего" },
|
||
{ value: c.inwork + (c.waiting || 0), label: "В работе" },
|
||
{ value: c.new, label: "Новые" },
|
||
{ value: c.done, label: "Выполнено" }
|
||
];
|
||
var h = "";
|
||
for (var i = 0; i < items.length; i++) {
|
||
h += '<div class="kt-ai-kpi"><span class="value">' + items[i].value +
|
||
'</span><span class="label">' + esc(items[i].label) + "</span></div>";
|
||
}
|
||
var strip = $("kpiStrip");
|
||
if (strip) strip.innerHTML = h;
|
||
var nav = $("navCount");
|
||
if (nav) nav.textContent = c[""];
|
||
}
|
||
|
||
var SEG_OPTIONS = [
|
||
{ key: "", label: "Все" },
|
||
{ key: "new", label: "Новые" },
|
||
{ key: "inwork", label: "В работе" },
|
||
{ key: "waiting", label: "Ожидание" },
|
||
{ key: "done", label: "Выполнено" }
|
||
];
|
||
|
||
function renderStatusSeg() {
|
||
var h = "";
|
||
for (var i = 0; i < SEG_OPTIONS.length; i++) {
|
||
var o = SEG_OPTIONS[i];
|
||
var active = state.statusFilter === o.key;
|
||
h += '<button class="seg" data-active="' + active + '" onclick="S.setSeg(\'' + o.key + '\')">' + esc(o.label) + "</button>";
|
||
}
|
||
var seg = $("statusSeg");
|
||
if (seg) seg.innerHTML = h;
|
||
}
|
||
|
||
function renderTable() {
|
||
var wrap = $("tableWrap");
|
||
if (!wrap) return;
|
||
var vis = filteredRequests(state.query);
|
||
if (vis.length === 0) {
|
||
var zeroData = state.requests.length === 0;
|
||
var icon = zeroData
|
||
? '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M9 16h6"/>'
|
||
: '<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>';
|
||
wrap.innerHTML = '<div class="kt-ai-empty"><div class="icon-spot"><svg class="kt-icon kt-icon-lg" viewBox="0 0 24 24">' + icon + "</svg></div>" +
|
||
"<span class=\"title\">" + (zeroData ? "Пока нет заявок" : "Ничего не найдено") + "</span>" +
|
||
"<span>" + (zeroData
|
||
? "Создайте первую заявку кнопкой «Новая заявка»"
|
||
: "Измените поиск или фильтр статуса") + "</span></div>";
|
||
return;
|
||
}
|
||
|
||
var h = '<table class="kt-ai-table"><thead><tr>' +
|
||
"<th>№</th><th>Оборудование / адрес</th><th>Статус</th><th>Приоритет</th><th>Создана</th><th>Кто</th>" +
|
||
"</tr></thead><tbody>";
|
||
|
||
for (var i = 0; i < vis.length; i++) {
|
||
var r = vis[i];
|
||
h += '<tr class="j-row" onclick="S.openView(\'' + esc(r.id) + '\')">' +
|
||
'<td class="j-id">' + esc(r.id) + "</td>" +
|
||
'<td><span class="j-name">' + esc(r.equipment) + '</span>' +
|
||
'<span class="j-sub">' + esc(r.address) + "</span></td>" +
|
||
"<td>" + statusPill(r.status) + "</td>" +
|
||
'<td class="j-prio">' + prioChip(r.priority) + "</td>" +
|
||
'<td class="j-date">' + esc(fmtDate(r.created)) + "</td>" +
|
||
'<td>' + esc(r.reporter) + "</td>" +
|
||
"</tr>";
|
||
}
|
||
h += "</tbody></table>";
|
||
wrap.innerHTML = h;
|
||
var el = $("visCount");
|
||
if (el) el.textContent = vis.length + " из " + state.requests.length;
|
||
}
|
||
|
||
function renderPrioSelect() {
|
||
var sel = $("prioSelect");
|
||
if (!sel) return;
|
||
var h = "";
|
||
for (var i = 0; i < PRIORITY_OPTIONS.length; i++) {
|
||
var o = PRIORITY_OPTIONS[i];
|
||
var active = state.priorityFilter === o.key ? "1" : "0";
|
||
h += '<option value="' + o.key + '"' + (active ? " selected" : "") + ">" + esc(o.label) + "</option>";
|
||
}
|
||
sel.innerHTML = h;
|
||
}
|
||
|
||
function renderAll() {
|
||
renderKpis();
|
||
renderStatusSeg();
|
||
renderPrioSelect();
|
||
renderTable();
|
||
}
|
||
|
||
/* ---------- Drawer ---------- */
|
||
function openDrawer() {
|
||
var dr = $("drawer");
|
||
if (dr) {
|
||
dr.style.display = "block";
|
||
dr.onkeydown = function (e) {
|
||
if (e.key === "Escape") closeDrawer();
|
||
};
|
||
dr.focus();
|
||
}
|
||
}
|
||
|
||
function closeDrawer() {
|
||
var dr = $("drawer");
|
||
if (dr) dr.style.display = "none";
|
||
state.openId = null;
|
||
state.drawerMode = "";
|
||
}
|
||
|
||
function fieldRow(k, v, multiline) {
|
||
return '<div><span class="k">' + esc(k) + '</span><span class="v' + (multiline ? " j-multiline" : "") + '">' + esc(v) + "</span></div>";
|
||
}
|
||
|
||
function renderView(id) {
|
||
var r = findRequest(id);
|
||
if (!r) return "";
|
||
var prio = PRIORITIES[r.priority] || { label: "—" };
|
||
var actions = "";
|
||
if (r.status === "new") {
|
||
actions += '<button class="kt-ai-btn" data-variant="primary" onclick="S.setStatus(\'' + esc(r.id) + '\',\'inwork\')">Взять в работу</button>';
|
||
}
|
||
if (r.status === "inwork") {
|
||
actions += '<button class="kt-ai-btn" onclick="S.setStatus(\'' + esc(r.id) + '\',\'waiting\')">Ожидание запчасти</button>';
|
||
actions += '<button class="kt-ai-btn" data-variant="primary" onclick="S.setStatus(\'' + esc(r.id) + '\',\'done\')">Завершить</button>';
|
||
}
|
||
if (r.status === "waiting") {
|
||
actions += '<button class="kt-ai-btn" data-variant="primary" onclick="S.setStatus(\'' + esc(r.id) + '\',\'inwork\')">Продолжить работу</button>';
|
||
}
|
||
if (r.status !== "done" && r.status !== "cancelled") {
|
||
actions += '<button class="kt-ai-btn" data-variant="danger" onclick="S.cancel(\'' + esc(r.id) + '\')">Отменить</button>';
|
||
}
|
||
actions += '<button class="kt-ai-btn" data-variant="ghost" onclick="S.closeDrawer()" style="margin-left:auto">Закрыть</button>';
|
||
|
||
return '<div class="j-drawer-head"><div class="t">' +
|
||
'<div class="id">' + esc(r.id) + "</div>" +
|
||
'<div class="kt-ai-h2" id="drawerTitle">' + esc(r.equipment) + "</div>" +
|
||
'<div style="margin-top:var(--kt-ai-space-3)">' + statusPill(r.status) + "</div>" +
|
||
"</div>" +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.closeDrawer()"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button></div>' +
|
||
'<div class="j-drawer-grid">' +
|
||
fieldRow("Адрес", r.address) +
|
||
fieldRow("Заявил", r.reporter) +
|
||
fieldRow("Дата", fmtDate(r.created)) +
|
||
fieldRow("Приоритет", prio.label) +
|
||
fieldRow("Обоснование", r.reason, true) +
|
||
"</div>" +
|
||
(actions ? '<div class="j-drawer-actions">' + actions + "</div>" : "");
|
||
}
|
||
|
||
function optionList(opts, selected) {
|
||
var h = "";
|
||
for (var i = 0; i < opts.length; i++) {
|
||
var o = opts[i];
|
||
h += "<option value=\"" + o.value + "\"" + (o.value === selected ? " selected" : "") + ">" + esc(o.label) + "</option>";
|
||
}
|
||
return h;
|
||
}
|
||
|
||
function renderNewForm() {
|
||
return '<div class="j-drawer-head"><div class="t">' +
|
||
'<div class="kt-ai-h2" id="drawerTitle">Новая заявка</div>' +
|
||
"</div>" +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.closeDrawer()"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button></div>' +
|
||
'<form id="reqForm" novalidate>' +
|
||
'<div class="j-form-grid">' +
|
||
'<div class="kt-ai-field"><label for="fEquip">Оборудование *</label>' +
|
||
'<input class="kt-ai-input" id="fEquip" placeholder="Например: Коммутатор" required></div>' +
|
||
'<div class="kt-ai-field"><label for="fAddr">Адрес / расположение *</label>' +
|
||
'<input class="kt-ai-input" id="fAddr" placeholder="Отдел, этаж, шкаф" required></div>' +
|
||
'<div class="kt-ai-field"><label for="fReporter">Заявил *</label>' +
|
||
'<input class="kt-ai-input" id="fReporter" placeholder="Имя и фамилия" required></div>' +
|
||
'<div class="kt-ai-field"><label for="fPrio">Приоритет</label>' +
|
||
'<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="fPrio">' +
|
||
optionList(PRIORITIES, "normal") +
|
||
"</select></div></div>" +
|
||
'<div class="kt-ai-field span-2"><label for="fReason">Обоснование *</label>' +
|
||
'<textarea class="kt-ai-textarea" id="fReason" placeholder="Что случилось, как проявляется" required></textarea></div>' +
|
||
"</div>" +
|
||
'<div class="j-drawer-actions">' +
|
||
'<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="S.closeDrawer()">Отмена</button>' +
|
||
'<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить заявку</button>' +
|
||
"</div></form>";
|
||
}
|
||
|
||
function bindForm() {
|
||
var form = $("reqForm");
|
||
if (!form) return;
|
||
form.onsubmit = function (e) {
|
||
e.preventDefault();
|
||
var equip = ($("fEquip").value || "").trim();
|
||
var addr = ($("fAddr").value || "").trim();
|
||
var rep = ($("fReporter").value || "").trim();
|
||
var reason = ($("fReason").value || "").trim();
|
||
var prio = $("fPrio").value || "normal";
|
||
var bad = 0;
|
||
setErr("fEquip", equip);
|
||
setErr("fAddr", addr);
|
||
setErr("fReporter", rep);
|
||
setErr("fReason", reason);
|
||
function setErr(fid, v) {
|
||
if (!v) bad++;
|
||
var el = $(fid);
|
||
var f = el ? el.closest(".kt-ai-field") : null;
|
||
if (f) f.setAttribute("data-error", v ? "false" : "true");
|
||
}
|
||
if (bad > 0) return;
|
||
var r = {
|
||
id: nextId(),
|
||
equipment: equip,
|
||
address: addr,
|
||
reporter: rep,
|
||
created: todayIso(0),
|
||
status: "new",
|
||
priority: prio,
|
||
reason: reason
|
||
};
|
||
state.requests.unshift(r);
|
||
saveRequests();
|
||
renderAll();
|
||
S.openView(r.id);
|
||
toast("Заявка " + r.id + " создана", "ok");
|
||
};
|
||
}
|
||
|
||
function renderEditForm(id) {
|
||
var r = findRequest(id);
|
||
if (!r) return "";
|
||
var can = {};
|
||
can.new = r.status === "new" || r.status === "inwork" || r.status === "waiting";
|
||
var h = '<div class="j-drawer-head"><div class="t">' +
|
||
'<div class="id">' + esc(r.id) + "</div>" +
|
||
'<div class="kt-ai-h2" id="drawerTitle">' + esc(r.equipment) + "</div>" +
|
||
"</div>" +
|
||
'<button class="kt-ai-icon-button" aria-label="Закрыть" onclick="S.closeDrawer()"><svg class="kt-icon" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button></div>' +
|
||
'<form id="reqForm" novalidate>' +
|
||
'<div class="j-form-grid">' +
|
||
'<div class="kt-ai-field"><label for="fEquip">Оборудование *</label>' +
|
||
'<input class="kt-ai-input" id="fEquip" value="' + esc(r.equipment) + '"></div>' +
|
||
'<div class="kt-ai-field"><label for="fAddr">Адрес / расположение *</label>' +
|
||
'<input class="kt-ai-input" id="fAddr" value="' + esc(r.address) + '"></div>' +
|
||
'<div class="kt-ai-field"><label for="fReporter">Заявил *</label>' +
|
||
'<input class="kt-ai-input" id="fReporter" value="' + esc(r.reporter) + '"></div>' +
|
||
'<div class="kt-ai-field"><label for="fPrio">Приоритет</label>' +
|
||
'<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="fPrio">' + optionList(PRIORITIES, r.priority) + "</select></div></div>" +
|
||
'<div class="kt-ai-field span-2"><label for="fReason">Обоснование *</label>' +
|
||
'<textarea class="kt-ai-textarea" id="fReason">' + esc(r.reason) + "</textarea></div>" +
|
||
(can.new
|
||
? '<div class="kt-ai-field span-2"><label for="fStat">Статус</label>' +
|
||
'<div class="kt-ai-select-wrap"><select class="kt-ai-select" id="fStat">' +
|
||
'<option value="new"' + (r.status === "new" ? " selected" : "") + ">Новая</option>" +
|
||
'<option value="inwork"' + (r.status === "inwork" ? " selected" : "") + ">В работе</option>" +
|
||
'<option value="waiting"' + (r.status === "waiting" ? " selected" : "") + ">Ожидает запчасти</option>" +
|
||
"</select></div></div>"
|
||
: "") +
|
||
"</div>" +
|
||
'<div class="j-drawer-actions">' +
|
||
'<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="S.closeDrawer()">Отмена</button>' +
|
||
'<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>' +
|
||
"</div></form>";
|
||
return h;
|
||
}
|
||
|
||
function bindEditForm(id) {
|
||
var form = $("reqForm");
|
||
if (!form) return;
|
||
form.onsubmit = function (e) {
|
||
e.preventDefault();
|
||
var r = findRequest(id);
|
||
if (!r) return;
|
||
var equip = ($("fEquip").value || "").trim();
|
||
var addr = ($("fAddr").value || "").trim();
|
||
var rep = ($("fReporter").value || "").trim();
|
||
var reason = ($("fReason").value || "").trim();
|
||
var bad = 0;
|
||
function setErr(fid, v) {
|
||
if (!v) bad++;
|
||
var el = $(fid);
|
||
var f = el ? el.closest(".kt-ai-field") : null;
|
||
if (f) f.setAttribute("data-error", v ? "false" : "true");
|
||
}
|
||
setErr("fEquip", equip);
|
||
setErr("fAddr", addr);
|
||
setErr("fReporter", rep);
|
||
setErr("fReason", reason);
|
||
if (bad > 0) return;
|
||
r.equipment = equip;
|
||
r.address = addr;
|
||
r.reporter = rep;
|
||
r.reason = reason;
|
||
r.priority = $("fPrio").value || r.priority;
|
||
var fStat = $("fStat");
|
||
if (fStat) r.status = fStat.value;
|
||
saveRequests();
|
||
renderAll();
|
||
S.openView(id);
|
||
toast("Заявка " + id + " обновлена", "ok");
|
||
};
|
||
}
|
||
|
||
/* ---------- Toast ---------- */
|
||
function toast(text, tone) {
|
||
var stack = $("toasts");
|
||
if (!stack) return;
|
||
var el = document.createElement("div");
|
||
el.className = "kt-ai-toast";
|
||
el.setAttribute("data-tone", tone || "");
|
||
el.textContent = text;
|
||
stack.appendChild(el);
|
||
setTimeout(function () {
|
||
if (el.parentNode) el.parentNode.removeChild(el);
|
||
}, 3600);
|
||
}
|
||
|
||
/* ---------- Публичный API ---------- */
|
||
var S = {};
|
||
|
||
S.goList = function () {
|
||
renderAll();
|
||
};
|
||
|
||
S.openView = function (id) {
|
||
state.openId = id;
|
||
state.drawerMode = "view";
|
||
var dr = $("drawer");
|
||
if (dr) {
|
||
dr.innerHTML = renderView(id);
|
||
openDrawer();
|
||
}
|
||
};
|
||
|
||
S.openNew = function () {
|
||
state.openId = null;
|
||
state.drawerMode = "new";
|
||
var dr = $("drawer");
|
||
if (dr) {
|
||
dr.innerHTML = renderNewForm();
|
||
openDrawer();
|
||
bindForm();
|
||
}
|
||
};
|
||
|
||
S.openEdit = function (id) {
|
||
state.openId = id;
|
||
state.drawerMode = "edit";
|
||
var dr = $("drawer");
|
||
if (dr) {
|
||
dr.innerHTML = renderEditForm(id);
|
||
openDrawer();
|
||
bindEditForm(id);
|
||
}
|
||
};
|
||
|
||
S.closeDrawer = closeDrawer;
|
||
|
||
S.setStatus = function (id, st) {
|
||
var r = findRequest(id);
|
||
if (!r) return;
|
||
if (!STATUSES[st]) return;
|
||
r.status = st;
|
||
saveRequests();
|
||
renderAll();
|
||
S.openView(id);
|
||
toast("Статус: " + (STATUSES[st] ? STATUSES[st].label : st), "ok");
|
||
};
|
||
|
||
S.cancel = function (id) {
|
||
var r = findRequest(id);
|
||
if (!r) return;
|
||
r.status = "cancelled";
|
||
saveRequests();
|
||
renderAll();
|
||
S.openView(id);
|
||
toast("Заявка " + id + " отменена", "warn");
|
||
};
|
||
|
||
S.setSeg = function (key) {
|
||
state.statusFilter = key;
|
||
renderKpis();
|
||
renderStatusSeg();
|
||
renderTable();
|
||
};
|
||
|
||
S.onSearch = function (v) {
|
||
state.query = v;
|
||
renderAll();
|
||
};
|
||
|
||
S.setPrio = function (v) {
|
||
state.priorityFilter = v;
|
||
renderAll();
|
||
};
|
||
|
||
S.toggleSidebar = function () {
|
||
var sb = $("sidebar");
|
||
if (sb) sb.setAttribute("data-open", sb.getAttribute("data-open") === "true" ? "false" : "true");
|
||
};
|
||
|
||
/* ---------- Старт ---------- */
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
initRequests();
|
||
renderAll();
|
||
});
|