320 lines
14 KiB
JavaScript
320 lines
14 KiB
JavaScript
(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 += '<button type="button" class="kt-ai-filter-chip" data-filter="' + items[i].key + '"' +
|
||
(state.status === items[i].key ? ' data-active="true"' : "") + ">" + items[i].label + "</button>";
|
||
}
|
||
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(
|
||
'<tr data-row="' + esc(r.id) + '" tabindex="0" style="cursor:pointer">' +
|
||
'<td style="font-weight:500">' + esc(r.id) + "</td>" +
|
||
"<td>" + esc(r.equip) + "</td>" +
|
||
"<td style=\"color:var(--kt-ai-fg-muted)\">" + esc(r.loc) + "</td>" +
|
||
'<td style="color:var(--kt-ai-fg-muted);font-variant-numeric:tabular-nums">' + fmtDate(r.created) + "</td>" +
|
||
'<td><span class="kt-ai-status-pill" data-status="' + s.pill + '">' + s.label + "</span></td>" +
|
||
"</tr>"
|
||
);
|
||
}
|
||
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();
|
||
})();
|