(function () { "use strict"; var LS_KEY = "zt-remont-ob-v1"; var STATUS = { new: { label: "Новая", pill: "info" }, work: { label: "В работе", pill: "warn" }, wait: { label: "Ожидает запчасти", pill: "muted" }, done: { label: "Выполнено", pill: "ok" }, cancel: { label: "Отменена", pill: "muted" } }; var SEED = [ { id: "Z-117", equip: "Маршрутизатор Cisco ISR 4451", loc: "ЦОД Астана, стойка 12", desc: "Утечка трафика на одном из модулей, требуется замена SFP.", created: "2026-09-21T09:14:00", updated: "2026-09-21T09:14:00", status: "done" }, { id: "Z-116", equip: "Коммутатор HPE Aruba 2930F", loc: "Филиал Алматы, серверная", desc: "Не определяется один порт агрегации, перезагрузка не помогла.", created: "2026-09-19T15:02:00", updated: "2026-09-19T15:02:00", status: "work" }, { id: "Z-115", equip: "ИБ-межсетевой экран FortiGate 200F", loc: "ЦОД Астана, зона DMZ", desc: "Падает производительность при сканировании, нужен апгрейд подписок.", created: "2026-09-18T11:26:00", updated: "2026-09-18T11:26:00", status: "wait" }, { id: "Z-114", equip: "UPS APC Smart-UPS SRT 3000", loc: "Филиал Караганда", desc: "Сигнализация замены батарей, резерв держит меньше 5 минут.", created: "2026-09-17T13:40:00", updated: "2026-09-17T13:40:00", status: "new" }, { id: "Z-113", equip: "Сервер Dell PowerEdge R750", loc: "ЦОД Астана, стойка 07", desc: "Неисправность контроллера RAID, деградирует массив.", created: "2026-09-15T10:05:00", updated: "2026-09-16T08:31:00", status: "work" }, { id: "Z-112", equip: "Кабельная муфта ВОЛС 12c", loc: "Магистраль Астана — Костанай", desc: "Повышенная оптическая потеря после работ подрядчика, требуется перепайка.", created: "2026-09-12T16:48:00", updated: "2026-09-14T10:12:00", status: "done" }, { id: "Z-111", equip: "Терминал доступа Juniper EX 3400", loc: "Филиал Шымкент", desc: "Перегрев, выключается через 20 минут под нагрузкой.", created: "2026-09-10T09:30:00", updated: "2026-09-10T09:30:00", status: "new" }, { id: "Z-110", equip: "Коммутатор Cisco Catalyst 9300", loc: "ЦОД Астана, стойка 21", desc: "Зависает процессор управления, требуется замена PSU.", created: "2026-09-08T14:22:00", updated: "2026-09-08T14:22:00", status: "cancel" }, { id: "Z-109", equip: "Антенна БС 19", loc: "Филиал Атырау", desc: "Слабый сигнал на секторах 2 и 3, требуется настройка азимута.", created: "2026-09-05T12:00:00", updated: "2026-09-07T09:45:00", status: "done" }, { id: "Z-108", equip: "Климатическая шкафа-система", loc: "ЦОД Астана, зал B", desc: "Не держит температуру выше 26°C в жаркий период.", created: "2026-09-03T08:55:00", updated: "2026-09-04T17:20:00", status: "work" } ]; var state = { status: "all", query: "", editMode: "new", editId: null }; var requests = load(); function load() { try { var raw = localStorage.getItem(LS_KEY); if (raw) { var arr = JSON.parse(raw); if (arr && arr.length) return arr; } } catch (e) { /* повреждённые данные — пересеваем */ } localStorage.setItem(LS_KEY, JSON.stringify(SEED)); return SEED.slice(); } function save() { localStorage.setItem(LS_KEY, JSON.stringify(requests)); } function nextId() { var max = 100; for (var i = 0; i < requests.length; i++) { var m = /Z-(\d+)/.exec(requests[i].id); if (m && parseInt(m[1], 10) > max) max = parseInt(m[1], 10); } return "Z-" + (max + 1); } function fmtDate(iso) { var d = new Date(iso); if (isNaN(d.getTime())) return "—"; var dd = String(d.getDate()).padStart(2, "0"); var mm = String(d.getMonth() + 1).padStart(2, "0"); return dd + "." + mm + "." + d.getFullYear(); } function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function rowMatches(r, st) { if (state.status !== "all" && r.status !== state.status) return false; if (state.query) { var q = state.query.toLowerCase(); var hay = (r.id + " " + r.equip + " " + r.loc + " " + r.desc).toLowerCase(); if (hay.indexOf(q) === -1) return false; } return true; } function renderFilter() { var bar = document.getElementById("filterbar"); var items = [{ key: "all", label: "Все" }]; for (var key in STATUS) items.push({ key: key, label: STATUS[key].label }); var html = ""; for (var i = 0; i < items.length; i++) { html += '"; } bar.innerHTML = html; } function renderTable() { var rows = document.getElementById("rows"); var empty = document.getElementById("empty"); var count = document.getElementById("row-count"); var out = []; var shown = 0; for (var i = 0; i < requests.length; i++) { var r = requests[i]; if (!rowMatches(r)) continue; shown++; var s = STATUS[r.status] || STATUS.new; out.push( '' + '' + esc(r.id) + "" + "" + esc(r.equip) + "" + "" + esc(r.loc) + "" + '' + fmtDate(r.created) + "" + '' + s.label + "" + "" ); } rows.innerHTML = out.join(""); var total = requests.length; count.textContent = state.status === "all" && !state.query ? total + " заявок" : shown + " из " + total; var visible = total > 0; rows.parentElement.querySelector("table").style.display = visible ? "" : "none"; empty.hidden = visible; } function renderKpi() { var all = requests.length, work = 0, wait = 0, done = 0; for (var i = 0; i < requests.length; i++) { var st = requests[i].status; if (st === "work") work++; if (st === "wait") wait++; if (st === "done") done++; } document.getElementById("kpi-all").textContent = all; document.getElementById("kpi-work").textContent = work; document.getElementById("kpi-wait").textContent = wait; document.getElementById("kpi-done").textContent = done; } function render() { renderKpi(); renderFilter(); renderTable(); } /* ---------- drawer ---------- */ function openDrawer(mode, id) { state.editMode = mode; state.editId = id || null; var drawer = document.getElementById("drawer"); var form = document.getElementById("form"); var detail = document.getElementById("detail"); var title = document.getElementById("drawer-title"); var sub = document.getElementById("drawer-sub"); drawer.hidden = false; document.body.style.overflow = "hidden"; form.hidden = false; detail.hidden = true; title.textContent = "Новая заявка"; sub.textContent = "Заполните поля, чтобы добавить запись в журнал"; document.getElementById("f-equip").value = ""; document.getElementById("f-loc").value = ""; document.getElementById("f-desc").value = ""; document.getElementById("f-status").value = "new"; document.getElementById("f-equip").focus(); } function openDetail(id) { var r = null; for (var i = 0; i < requests.length; i++) { if (requests[i].id === id) { r = requests[i]; break; } } if (!r) return; state.editMode = "detail"; state.editId = id; var drawer = document.getElementById("drawer"); var form = document.getElementById("form"); var detail = document.getElementById("detail"); drawer.hidden = false; document.body.style.overflow = "hidden"; form.hidden = true; detail.hidden = false; document.getElementById("drawer-title").textContent = r.id + " · " + r.equip; document.getElementById("drawer-sub").textContent = (STATUS[r.status] ? STATUS[r.status].label : r.status) + " · создана " + fmtDate(r.created); document.getElementById("d-desc").textContent = r.desc; document.getElementById("d-equip").textContent = r.equip; document.getElementById("d-loc").textContent = r.loc; document.getElementById("d-date").textContent = fmtDate(r.created); document.getElementById("d-upd").textContent = fmtDate(r.updated); document.getElementById("d-status").value = r.status; document.getElementById("drawer-close").focus(); } function closeDrawer() { document.getElementById("drawer").hidden = true; document.body.style.overflow = ""; } function toast(text) { var stack = document.getElementById("toasts"); var el = document.createElement("div"); el.className = "kt-ai-toast"; el.textContent = text; stack.appendChild(el); setTimeout(function () { if (el.parentElement) el.parentElement.removeChild(el); }, 2600); } /* ---------- события ---------- */ document.getElementById("filterbar").addEventListener("click", function (e) { var chip = e.target.closest("[data-filter]"); if (!chip) return; state.status = chip.getAttribute("data-filter"); render(); }); document.getElementById("search").addEventListener("input", function () { state.query = this.value.trim(); renderTable(); }); document.getElementById("btn-new").addEventListener("click", function () { openDrawer("new"); }); document.getElementById("rows").addEventListener("click", function (e) { var tr = e.target.closest("tr[data-row]"); if (!tr) return; openDetail(tr.getAttribute("data-row")); }); document.getElementById("rows").addEventListener("keydown", function (e) { if (e.key !== "Enter") return; var tr = e.target.closest("tr[data-row]"); if (tr) openDetail(tr.getAttribute("data-row")); }); document.getElementById("drawer-close").addEventListener("click", closeDrawer); document.getElementById("btn-cancel").addEventListener("click", closeDrawer); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !document.getElementById("drawer").hidden) closeDrawer(); }); document.getElementById("form").addEventListener("submit", function (e) { e.preventDefault(); var rec = { id: nextId(), equip: document.getElementById("f-equip").value.trim(), loc: document.getElementById("f-loc").value.trim(), desc: document.getElementById("f-desc").value.trim(), status: document.getElementById("f-status").value, created: new Date().toISOString(), updated: new Date().toISOString() }; if (!rec.equip || !rec.loc || !rec.desc) return; requests.unshift(rec); save(); state.status = "all"; state.query = ""; document.getElementById("search").value = ""; closeDrawer(); render(); toast("Заявка " + rec.id + " добавлена"); }); document.getElementById("btn-export").addEventListener("click", function () { var rows = []; for (var i = 0; i < requests.length; i++) { if (!rowMatches(requests[i])) continue; var r = requests[i]; rows.push({ "Заявка": r.id, "Оборудование": r.equip, "Расположение": r.loc, "Описание": r.desc, "Создана": fmtDate(r.created), "Статус": STATUS[r.status] ? STATUS[r.status].label : r.status }); } if (!rows.length) { toast("Нечего выгружать — по фильтрам нет записей"); return; } var wb = XLSX.utils.json_to_sheet(rows); var widths = ["Заявка", "Оборудование", "Расположение", "Описание"].map(function (h) { var w = h.length; rows.forEach(function (row) { var v = String(row[h] || ""); if (v.length > w) w = v.length; }); return { wch: Math.min(w + 2, 60) }; }); widths.push({ wch: 12 }, { wch: 20 }); wb["!cols"] = widths; var sheetName = state.status === "all" ? "Заявки" : STATUS[state.status].label; var wbout = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wbout, wb, sheetName.slice(0, 31)); var d = new Date(); var name = "Journal_" + String(d.getDate()).padStart(2, "0") + String(d.getMonth() + 1).padStart(2, "0") + d.getFullYear() + ".xlsx"; XLSX.writeFile(wbout, name); toast("Выгружено записей: " + rows.length); }); document.getElementById("btn-apply").addEventListener("click", function () { if (!state.editId) return; for (var i = 0; i < requests.length; i++) { if (requests[i].id === state.editId) { requests[i].status = document.getElementById("d-status").value; requests[i].updated = new Date().toISOString(); break; } } save(); closeDrawer(); render(); toast("Статус обновлён"); }); render(); })();