"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 '' + esc(st.label) + ""; } function prioChip(p) { var pr = PRIORITIES[p] || PRIORITIES.normal; return '' + esc(pr.label) + ""; } 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 += '
' + items[i].value + '' + esc(items[i].label) + "
"; } 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 += '"; } 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 ? '' : ''; wrap.innerHTML = '
' + icon + "
" + "" + (zeroData ? "Пока нет заявок" : "Ничего не найдено") + "" + "" + (zeroData ? "Создайте первую заявку кнопкой «Новая заявка»" : "Измените поиск или фильтр статуса") + "
"; return; } var h = '' + "" + ""; for (var i = 0; i < vis.length; i++) { var r = vis[i]; h += '' + '" + '" + "" + '" + '" + '" + ""; } h += "
№Оборудование / адресСтатусПриоритетСозданаКто
' + esc(r.id) + "' + esc(r.equipment) + '' + '' + esc(r.address) + "" + statusPill(r.status) + "' + prioChip(r.priority) + "' + esc(fmtDate(r.created)) + "' + esc(r.reporter) + "
"; 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 += '"; } 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 '
' + esc(k) + '' + esc(v) + "
"; } function renderView(id) { var r = findRequest(id); if (!r) return ""; var prio = PRIORITIES[r.priority] || { label: "—" }; var actions = ""; if (r.status === "new") { actions += ''; } if (r.status === "inwork") { actions += ''; actions += ''; } if (r.status === "waiting") { actions += ''; } if (r.status !== "done" && r.status !== "cancelled") { actions += ''; } actions += ''; return '
' + '
' + esc(r.id) + "
" + '
' + esc(r.equipment) + "
" + '
' + statusPill(r.status) + "
" + "
" + '
' + '
' + fieldRow("Адрес", r.address) + fieldRow("Заявил", r.reporter) + fieldRow("Дата", fmtDate(r.created)) + fieldRow("Приоритет", prio.label) + fieldRow("Обоснование", r.reason, true) + "
" + (actions ? '
' + actions + "
" : ""); } function optionList(opts, selected) { var h = ""; for (var i = 0; i < opts.length; i++) { var o = opts[i]; h += ""; } return h; } function renderNewForm() { return '
' + '
Новая заявка
' + "
" + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
" + '
' + '
' + "
" + '
' + '' + '' + "
"; } 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 = '
' + '
' + esc(r.id) + "
" + '
' + esc(r.equipment) + "
" + "
" + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
" + '
' + '
" + (can.new ? '
' + '
" : "") + "
" + '
' + '' + '' + "
"; 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(); });