// Журнал заявок на ремонт оборудования. // Данные хранятся в localStorage; при первом открытии подставляются исходные записи. var STATUS_ORDER = [ "new", "assigned", "in_progress", "awaiting_parts", "resolved", "cancelled" ]; var STATUS_LABEL = { new: "Новая", assigned: "Назначена", in_progress: "В работе", awaiting_parts: "Ждёт запчасть", resolved: "Завершена", cancelled: "Отменена" }; var STATUS_TONE = { new: "info", assigned: "info", in_progress: "warn", awaiting_parts: "risk", resolved: "ok", cancelled: "muted" }; var SEED = [ { id: "РМ-2104", equipment: "Серверная стойка A-3", place: "ЦОД, 1 этаж, стойка 12", cls: "Срочный", status: "in_progress", created: "2026-09-18", updated: "2026-09-21", resp: "И. Ахметов", desc: "Сбой охлаждения: температура в стойке выше 30 °C, датчики показывают 38 °C. Требуется диагностика блоков кондиционирования." }, { id: "РМ-2103", equipment: "Транслирующая станция «Алматы-4»", place: "ДК-4, уровень 12", cls: "Срочный", status: "awaiting_parts", created: "2026-09-17", updated: "2026-09-20", resp: "С. Оспанов", desc: "Отказ модуля питания. Станция переведена на резервный канал, основной канал выведен из работы." }, { id: "РМ-2102", equipment: "Видеонаблюдение, парковка П-2", place: "Штаб, парковка 2", cls: "Повседневный", status: "new", created: "2026-09-21", updated: "2026-09-21", resp: null, desc: "Камера №7 не передаёт изображение. Блок питания в порядке, сигнал отсутствует." }, { id: "РМ-2101", equipment: "Принтер HP LaserJet M404", place: "БЦ «Тау Самал», кабинет 204", cls: "Повседневный", status: "resolved", created: "2026-09-10", updated: "2026-09-14", resp: "А. Нурланов", desc: "Замятие бумаги. Заменён роликовый узел подачи, тестовая печать выполнена." }, { id: "РМ-2100", equipment: "IP-телефон Cisco 8845", place: "Кабинет 315", cls: "Повседневный", status: "assigned", created: "2026-09-19", updated: "2026-09-21", resp: "М. Тлеубердиева", desc: "Не регистрируется на шлюзе после планового обновления прошивки." }, { id: "РМ-2099", equipment: "Климат-установка DX-140", place: "ЦОД, серверный зал 2", cls: "Срочный", status: "in_progress", created: "2026-09-15", updated: "2026-09-21", resp: "Т. Бекова", desc: "Течь фреонового контура. Выполнена локализация утечки, ожидается дозаправка." }, { id: "РМ-2098", equipment: "Рабочая станция, отдел поддержки", place: "Кабинет 108", cls: "Повседневный", status: "resolved", created: "2026-09-08", updated: "2026-09-12", resp: "М. Тлеубердиева", desc: "Зависания системы при загрузке. Перенесена ОС на новый диск." }, { id: "РМ-2097", equipment: "Патч-панель ОД-2", place: "Шкаф ОД-2, 2 этаж", cls: "Повседневный", status: "awaiting_parts", created: "2026-09-11", updated: "2026-09-18", resp: "И. Ахметов", desc: "Обрыв четырёх портов на секции 3. Заказать патч-корды и портовое исполнение." }, { id: "РМ-2096", equipment: "Система контроля доступа, крыло Б", place: "Здание Б, этаж 1", cls: "Срочный", status: "assigned", created: "2026-09-20", updated: "2026-09-21", resp: "С. Оспанов", desc: "Турникет №2 не считывает RFID-карты. Коммуникация с контроллером не отвечает." }, { id: "РМ-2095", equipment: "Кофемашина Breville", place: "Столовая, 3 этаж", cls: "Повседневный", status: "cancelled", created: "2026-09-05", updated: "2026-09-09", resp: null, desc: "Заявка дублирует РМ-2090, снята с исполнения." }, { id: "РМ-2094", equipment: "Проектор Epson EB-2250", place: "Зал заседаний 4", cls: "Повседневный", status: "resolved", created: "2026-09-02", updated: "2026-09-06", resp: "А. Нурланов", desc: "Потускнение картинки, замена лампы выполнена, яркость в норме." }, { id: "РМ-2093", equipment: "Генератор ГРА-250", place: "Подвал, генераторная", cls: "Срочный", status: "in_progress", created: "2026-09-16", updated: "2026-09-20", resp: "Т. Бекова", desc: "Не запускается при плановом тесте. Проверка системы запуска и топливного контура." }, { id: "РМ-2092", equipment: "Ноутбук Lenovo ThinkPad T14", place: "Кабинет 220", cls: "Повседневный", status: "new", created: "2026-09-22", updated: "2026-09-22", resp: null, desc: "Заряд аккумулятора держит 20 минут, замена батареи требуется." }, { id: "РМ-2091", equipment: "ОПТП-трасса, сегмент Д-3", place: "Линия Д-3, опоры 12–18", cls: "Срочный", status: "awaiting_parts", created: "2026-09-13", updated: "2026-09-19", resp: "С. Оспанов", desc: "Повреждение на сегменте после работ подрядчика. Ждём кабельный запас, согласован поставщик." }, { id: "РМ-2090", equipment: "Кофемашина Breville", place: "Столовая, 3 этаж", cls: "Повседневный", status: "resolved", created: "2026-08-30", updated: "2026-09-04", resp: "М. Тлеубердиева", desc: "Промывка и чистка, замена уплотнителя. Работает в штатном режиме." }, { id: "РМ-2089", equipment: "Коммутатор Cisco 3750", place: "Шкаф ОД-1, порт 2", cls: "Повседневный", status: "assigned", created: "2026-09-18", updated: "2026-09-21", resp: "А. Нурланов", desc: "Порт 2 гаснет при подключении терминала. Заменён кабель, порт в норме." }, { id: "РМ-2088", equipment: "Климат-установка DX-110", place: "ЦОД, серверный зал 1", cls: "Срочный", status: "resolved", created: "2026-08-25", updated: "2026-09-01", resp: "Т. Бекова", desc: "Заменён компрессор, температура в зале стабилизирована." }, { id: "РМ-2087", equipment: "Кресло оператора, рабочее место 7", place: "Оперативный зал, место 7", cls: "Повседневный", status: "new", created: "2026-09-22", updated: "2026-09-22", resp: null, desc: "Не регулируется по высоте." } ]; var LS_KEY = "zhurnal-zayavok-v1"; var state = { reqs: null, filter: "all", search: "", sortDesc: true, selectedId: null }; function $(id) { return document.getElementById(id); } function esc(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function pad2(n) { var s = String(n); return s.length < 2 ? "0" + s : s; } function fmtDate(d) { if (!d) return "—"; var parts = String(d).split("-"); if (parts.length !== 3) return String(d); return pad2(parseInt(parts[2], 10)) + "." + pad2(parseInt(parts[1], 10)) + "." + parts[0]; } function nextId() { var max = 2086; for (var i = 0; i < state.reqs.length; i++) { var m = /^РМ-(\d+)$/.exec(state.reqs[i].id || ""); if (m) { var n = parseInt(m[1], 10); if (n > max) max = n; } } return "РМ-" + (max + 1); } function loadReqs() { try { var raw = window.localStorage.getItem(LS_KEY); if (raw) { var arr = JSON.parse(raw); if (Object.prototype.toString.call(arr) === "[object Array]") return arr; } } catch (e) { /* повреждённое хранение — пересоздаём */ } return null; } function saveReqs() { try { window.localStorage.setItem(LS_KEY, JSON.stringify(state.reqs)); } catch (e) { /* приватный режим и т.п. */ } } function cloneSeed() { var out = []; for (var i = 0; i < SEED.length; i++) { var r = {}; for (var k in SEED[i]) r[k] = SEED[i][k]; out.push(r); } return out; } function resetToSeed() { state.reqs = cloneSeed(); saveReqs(); } // Фильтр: статус + поиск, сортировка по дате создания. function visibleReqs() { var q = state.search.trim().toLowerCase(); var out = []; for (var i = 0; i < state.reqs.length; i++) { var r = state.reqs[i]; if (state.filter !== "all" && r.status !== state.filter) continue; if (q) { var hay = (r.id + " " + r.equipment + " " + r.place + " " + (r.desc || "")).toLowerCase(); if (hay.indexOf(q) === -1) continue; } out.push(r); } out.sort(function (a, b) { var da = a.created || "", db = b.created || ""; if (da < db) return state.sortDesc ? 1 : -1; if (da > db) return state.sortDesc ? -1 : 1; return a.id < b.id ? 1 : a.id > b.id ? -1 : 0; }); return out; } function countByStatus() { var m = {}; for (var i = 0; i < STATUS_ORDER.length; i++) m[STATUS_ORDER[i]] = 0; for (var j = 0; j < state.reqs.length; j++) { var s = state.reqs[j].status; if (Object.prototype.hasOwnProperty.call(m, s)) m[s]++; } return m; } function findReq(id) { for (var i = 0; i < state.reqs.length; i++) { if (state.reqs[i].id === id) return state.reqs[i]; } return null; } function pillHtml(status) { var tone = STATUS_TONE[status] || "info"; return '' + esc(STATUS_LABEL[status] || status) + ""; } // ———————————————————————————————— // Рендер // ———————————————————————————————— function renderFilters() { var c = countByStatus(); var host = $("statusFilters"); if (!host) return; var parts = []; parts.push('"); for (var i = 0; i < STATUS_ORDER.length; i++) { var s = STATUS_ORDER[i]; var active = state.filter === s ? "true" : "false"; var label = statusWordForFilter(s); parts.push('"); } host.innerHTML = parts.join(""); } function statusWordForFilter(s) { var short = { new: "Новые", assigned: "Назначенные", in_progress: "В работе", awaiting_parts: "Ждут запчасть", resolved: "Завершённые", cancelled: "Отменённые" }; return short[s] || STATUS_LABEL[s]; } function renderTable() { var body = $("reqBody"); if (!body) return; var rows = visibleReqs(); if (rows.length === 0) { body.innerHTML = ""; var empty = $("emptyState"); if (empty) empty.hidden = false; } else { var empty2 = $("emptyState"); if (empty2) empty2.hidden = true; var html = []; for (var i = 0; i < rows.length; i++) { html.push(rowHtml(rows[i])); } body.innerHTML = html.join(""); } updateCount(); } function rowHtml(r) { var selected = state.selectedId === r.id ? "true" : "false"; var cls = esc(r.cls || "—"); return '' + '
' + esc(r.id) + "" + '' + esc(r.equipment || "") + "
" + "" + pillHtml(r.status) + "" + '' + cls + "" + '' + fmtDate(r.created) + "" + ""; } function renderDetail() { var host = $("detailPane"); if (!host) return; var r = state.selectedId ? findReq(state.selectedId) : null; if (!r) { host.innerHTML = '
' + '' + '
Выберите заявку
' + '
Нажмите на строку в списке, чтобы увидеть детали и сменить статус.
' + "
"; return; } var resp = r.resp ? esc(r.resp) : "—"; var cls = esc(r.cls || "—"); host.innerHTML = '
' + '
' + '
' + esc(r.id) + "
" + '
' + '
' + esc(r.equipment || "") + "
" + pillHtml(r.status) + "
" + "
" + '
' + detailRow("Место", r.place) + detailRow("Класс", cls) + detailRow("Создана", fmtDate(r.created)) + detailRow("Обновлена", fmtDate(r.updated)) + detailRow("Исполнитель", resp) + "
" + '
Описание
' + '

' + (r.desc ? esc(r.desc) : "—") + "

" + '
' + '" + "
"; var sel = $("statusSelect"); if (sel) { sel.addEventListener("change", onStatusChange); } } function detailRow(k, v) { return '
' + esc(k) + '' + esc(v) + "
"; } function statusOptions(current) { var out = []; for (var i = 0; i < STATUS_ORDER.length; i++) { var s = STATUS_ORDER[i]; var selected = s === current ? "selected" : ""; out.push('"); } return out.join(""); } function updateCount() { var total = state.reqs.length; var shown = visibleReqs().length; var el = $("tableCount"); if (!el) return; if (state.filter === "all" && !state.search.trim()) { el.textContent = "Показаны все заявки: " + shown; } else { el.textContent = "Показано " + shown + " из " + total; } } function updateKpi() { var c = countByStatus(); setKpi("kpiTotal", state.reqs.length); setKpi("kpiNew", c.new); setKpi("kpiWork", c.in_progress + c.assigned); setKpi("kpiDone", c.resolved + c.cancelled); } function setKpi(id, n) { var el = $(id); if (el) el.textContent = String(n); } function renderAll() { renderFilters(); renderTable(); renderDetail(); updateKpi(); updateClearButton(); } function updateClearButton() { var btn = $("clearFiltersBtn"); if (!btn) return; var any = state.filter !== "all" || state.search.trim() !== ""; btn.hidden = !any; } // ———————————————————————————————— // Действия // ———————————————————————————————— function selectRow(id) { state.selectedId = id; syncRowSelection(); renderDetail(); } function syncRowSelection() { var tbody = $("reqBody"); if (!tbody) return; var rows = tbody.querySelectorAll("tr.selectable"); for (var i = 0; i < rows.length; i++) { rows[i].setAttribute("data-selected", rows[i].getAttribute("data-id") === state.selectedId ? "true" : "false"); } } function setFilter(s) { state.filter = s; state.selectedId = null; renderFilters(); renderTable(); renderDetail(); updateKpi(); updateClearButton(); } function onStatusChange(e) { var sel = e.target; if (!sel || state.selectedId == null) return; var r = findReq(state.selectedId); if (!r) return; r.status = sel.value; r.updated = todayStr(); saveReqs(); renderAll(); toast("Статус " + esc(r.id) + ": " + STATUS_LABEL[sel.value]); } function todayStr() { var d = new Date(); return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()); } function toast(text) { var stack = $("toasts"); if (!stack) return; var t = document.createElement("div"); t.className = "kt-ai-toast"; t.setAttribute("data-tone", "ok"); t.textContent = text; stack.appendChild(t); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3500); } function openModal() { var m = $("modal"); if (!m) return; m.hidden = false; $("reqForm").reset(); if ($("fEquipment")) $("fEquipment").focus(); } function closeModal() { var m = $("modal"); if (m) m.hidden = true; } function doReset() { resetToSeed(); state.filter = "all"; state.search = ""; state.selectedId = null; if ($("searchInput")) $("searchInput").value = ""; renderAll(); toast("Список восстановлен"); } // ———————————————————————————————— // Обработчики // ———————————————————————————————— function onFiltersClick(e) { var chip = e.target; while (chip && chip !== document && !(chip.classList && chip.classList.contains("status-chip"))) { chip = chip.parentNode; } if (!chip || chip === document) return; setFilter(chip.getAttribute("data-status") || "all"); } function onSortClick() { state.sortDesc = !state.sortDesc; var lbl = $("sortLabel"); if (lbl) lbl.textContent = state.sortDesc ? "Сначала новые" : "Сначала старые"; renderTable(); } function onFormSubmit(e) { e.preventDefault(); var eq = $("fEquipment").value.trim(); if (!eq) { $("fEquipment").focus(); return; } var today = todayStr(); var r = { id: nextId(), equipment: eq, place: $("fPlace").value.trim() || null, cls: $("fClass").value, status: "new", created: today, updated: today, resp: null, desc: $("fDesc").value.trim() || null }; state.reqs.unshift(r); saveReqs(); state.selectedId = r.id; state.filter = "all"; state.search = ""; if ($("searchInput")) $("searchInput").value = ""; renderAll(); closeModal(); toast("Заявка " + r.id + " создана"); } function bindEvents() { $("reqBody").addEventListener("click", function (e) { var tr = e.target; while (tr && tr !== this && !(tr.tagName === "TR" && tr.classList.contains("selectable"))) { tr = tr.parentNode; } if (!tr || tr === this) return; selectRow(tr.getAttribute("data-id")); }); $("statusFilters").addEventListener("click", onFiltersClick); $("sortBtn").addEventListener("click", onSortClick); $("searchInput").addEventListener("input", function (e) { state.search = e.target.value || ""; renderTable(); updateClearButton(); }); $("newBtn").addEventListener("click", openModal); $("cancelBtn").addEventListener("click", closeModal); $("resetBtn").addEventListener("click", doReset); $("clearFiltersBtn").addEventListener("click", doClearFilters); $("emptyClearBtn").addEventListener("click", doClearFilters); $("modal").addEventListener("click", function (e) { if (e.target === this) closeModal(); }); $("reqForm").addEventListener("submit", onFormSubmit); $("exportBtn").addEventListener("click", exportExcel); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !$("modal").hidden) closeModal(); }); } function csvCell(v) { var s = v == null ? "" : String(v); return '"' + s.replace(/"/g, '""') + '"'; } function exportExcel() { var rows = visibleReqs(); if (rows.length === 0) { toast("Нечего экспортировать"); return; } var head = ["Номер", "Оборудование", "Место", "Класс", "Статус", "Создана", "Обновлена", "Исполнитель", "Описание"]; var lines = [head.map(csvCell).join(",")]; for (var i = 0; i < rows.length; i++) { var r = rows[i]; lines.push([ r.id, r.equipment, r.place, r.cls, STATUS_LABEL[r.status] || r.status, fmtDate(r.created), fmtDate(r.updated), r.resp || "", r.desc || "" ].map(csvCell).join(",")); } var blob = new Blob([lines.join("\r\n")], { type: "application/vnd.ms-excel" }); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = "zayavki-remont-" + todayStr() + ".xls"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); toast("Выгружено заявок: " + rows.length); } function doClearFilters() { state.filter = "all"; state.search = ""; state.selectedId = null; if ($("searchInput")) $("searchInput").value = ""; renderAll(); } function init() { var stored = loadReqs(); state.reqs = stored || cloneSeed(); if (!stored) saveReqs(); bindEvents(); renderAll(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); }