zhurnal-zayavok-na-remont-ob/script.js

592 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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();
});