/* Журнал заявок на ремонт оборудования. Данные в localStorage. */ (function () { "use strict"; var STORAGE_KEY = "remont-zayavki-v1"; var ICON_BASE = "design-system/icons/kt-ai-lucide-sprite.svg"; var STATUSES = [ { id: "new", label: "Новая", tone: "info" }, { id: "work", label: "В работе", tone: "warn" }, { id: "wait", label: "Ожидает запчасть", tone: "risk" }, { id: "planned", label: "Запланирована", tone: "info" }, { id: "done", label: "Исправлена", tone: "ok" } ]; /* ---------- даты ---------- */ function pad(n) { return (n < 10 ? "0" : "") + n; } function dstr(offset) { var d = new Date(); d.setDate(d.getDate() + (offset || 0)); return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()); } function fmtDate(iso) { if (!iso) return "—"; var p = iso.split("-"); return p.length === 3 ? p[2] + "." + p[1] + "." + p[0] : iso; } function fmtWhen(iso) { var d = new Date(iso); if (isNaN(d.getTime())) return String(iso); return pad(d.getDate()) + "." + pad(d.getMonth() + 1) + "." + d.getFullYear() + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()); } function daysDiff(iso) { var a = new Date(dstr(0) + "T00:00:00").getTime(); var b = new Date(iso.replace("T", " ") + ":00").getTime(); return Math.round((b - a) / 86400000); } function deadlineLabel(r) { if (r.status === "done") return fmtDate(r.deadline) + " · закрыта"; var df = daysDiff(r.deadline); if (df < 0) return fmtDate(r.deadline) + " · просрочено"; if (df === 0) return fmtDate(r.deadline) + " · сегодня"; if (df === 1) return fmtDate(r.deadline) + " · завтра"; return fmtDate(r.deadline) + " · через " + df + " дн."; } /* ---------- примерные данные ---------- */ var SEED_ITEMS = [ { id: 1, person: "Айдар Сериков", what: "Принтер в переговорной «Сары-Арка» — замятие бумаги", deadline: -3, status: "work", submitted: -8, note: "Требуется замена картриджа. Обслуживание по средам." }, { id: 2, person: "Нурсултан Ахметов", what: "Телевизор в актовом зале без изображения", deadline: 0, status: "wait", submitted: -5, note: "Ожидается поставка блока питания." }, { id: 3, person: "Дарья Ким", what: "Кондиционер в кабинете 4-12 не холодит", deadline: 2, status: "work", submitted: -2, note: "Требуется дозаправка фреоном." }, { id: 4, person: "Ержан Нурланов", what: "Кресло руководителя — сломан подлокотник", deadline: 3, status: "planned", submitted: -4, note: "Назначено на пятницу 10:00." }, { id: 5, person: "Айгерим Тлеубердиева", what: "Компьютер (рабочее место 2-07) не включается", deadline: -1, status: "work", submitted: -3, note: "Диагностика блока питания." }, { id: 6, person: "Мадина Исмагулова", what: "Сканер в отделе кадров рвёт листы", deadline: 6, status: "new", submitted: 0, note: "" }, { id: 7, person: "Тимур Рахимов", what: "Монитор в переговорной №3 — вертикальные полосы", deadline: -2, status: "done", submitted: -12, note: "Заменён шлейф матрицы." }, { id: 8, person: "Айбек Тастанбеков", what: "Система видеонаблюдения — камера у входа не пишет", deadline: 1, status: "work", submitted: -1, note: "Требуется замена HDD регистратора." }, { id: 9, person: "Гузель Омарова", what: "Кофемашина в комнате отдыха течёт из-под чашки", deadline: -3, status: "done", submitted: -9, note: "Заменён уплотнитель, резервуар промыт." }, { id: 10, person: "Олеся Бекенова", what: "Свет в коридоре 3-го этажа — мигает группа ламп", deadline: 14, status: "new", submitted: 0, note: "" } ]; var SEED_LOG = [ { offset: -12, time: "13:10", user: "Тимур Рахимов", what: "Заявка №7 подана" }, { offset: -9, time: "10:25", user: "Гузель Омарова", what: "Заявка №9 подана" }, { offset: -8, time: "09:12", user: "Айдар Сериков", what: "Заявка №1 подана" }, { offset: -8, time: "11:00", user: "Служба", what: "Заявка №7 — статус «В работе»" }, { offset: -6, time: "14:30", user: "Служба", what: "Заявка №1 — статус «В работе»" }, { offset: -5, time: "08:30", user: "Служба", what: "Заявка №9 — статус «В работе»" }, { offset: -5, time: "11:05", user: "Нурсултан Ахметов", what: "Заявка №2 подана" }, { offset: -4, time: "08:45", user: "Ержан Нурланов", what: "Заявка №4 подана" }, { offset: -3, time: "10:30", user: "Айгерим Тлеубердиева", what: "Заявка №5 подана" }, { offset: -3, time: "12:10", user: "Служба", what: "Заявка №9 — статус «Исправлена»" }, { offset: -2, time: "16:20", user: "Дарья Ким", what: "Заявка №3 подана" }, { offset: -1, time: "09:00", user: "Служба", what: "Заявка №5 — статус «В работе»" }, { offset: -1, time: "11:30", user: "Нурсултан Ахметов", what: "Заявка №2 — статус «Ожидает запчасть»" }, { offset: -1, time: "12:00", user: "Служба", what: "Заявка №4 — статус «Запланирована»" }, { offset: -1, time: "17:05", user: "Айбек Тастанбеков", what: "Заявка №8 подана" }, { offset: 0, time: "08:15", user: "Мадина Исмагулова", what: "Заявка №6 подана" }, { offset: 0, time: "09:40", user: "Служба", what: "Заявка №8 — статус «В работе»" }, { offset: 0, time: "10:05", user: "Служба", what: "Заявка №3 — статус «В работе»" }, { offset: 0, time: "11:40", user: "Олеся Бекенова", what: "Заявка №10 подана" } ]; function applySeedDates() { for (var i = 0; i < SEED_ITEMS.length; i++) { var s = SEED_ITEMS[i]; s.submitted = dstr(s.submitted); s.deadline = dstr(s.deadline); delete s.submittedOffset; delete s.deadlineOffset; } return SEED_ITEMS; } function buildSeed() { var items = []; applySeedDates(); for (var i = 0; i < SEED_ITEMS.length; i++) { var s = SEED_ITEMS[i]; items.push({ id: s.id, person: s.person, what: s.what, submitted: s.submitted, deadline: s.deadline, status: s.status, note: s.note, events: seedEvents(s) }); } var log = []; for (var j = 0; j < SEED_LOG.length; j++) { var e = SEED_LOG[j]; log.push({ when: dstr(e.offset) + "T" + e.time + ":00", user: e.user, what: e.what }); } log.sort(function (a, b) { return a.when < b.when ? 1 : -1; }); return { items: items, log: log }; } function seedEvents(s) { var ev = [{ when: s.submitted + "T09:15:00", user: s.person, what: "заявка подана" }]; var workDay = shiftDay(s.submitted, 1); var nextDay = shiftDay(workDay, 1); if (s.status === "done") ev.push({ when: s.deadline + "T15:00:00", user: "Служба", what: "статус: Исправлена" }); else if (s.status === "work") ev.push({ when: workDay + "T10:00:00", user: "Служба", what: "статус: В работе" }); else if (s.status === "wait") { ev.push({ when: workDay + "T10:00:00", user: "Служба", what: "статус: В работе" }); ev.push({ when: nextDay + "T10:00:00", user: "Служба", what: "статус: Ожидает запчасть" }); } else if (s.status === "planned") ev.push({ when: workDay + "T10:00:00", user: "Служба", what: "статус: Запланирована" }); return ev; } function shiftDay(base, plus) { var d = new Date(base + "T00:00:00"); d.setDate(d.getDate() + plus); return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()); } function load() { try { var raw = localStorage.getItem(STORAGE_KEY); if (raw) { var d = JSON.parse(raw); if (d && d.items) return d; } } catch (err) {} var fresh = buildSeed(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh)); } catch (err2) {} return fresh; } function persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(S)); } catch (err) { toast("Не удалось сохранить данные", "risk"); } } function resetData() { S = buildSeed(); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(S)); } catch (err) {} state.status = "all"; state.q = ""; var si = $("searchInput"); if (si) si.value = ""; renderAll(); toast("Данные возвращены к исходным", "ok"); } var S = load(); var state = { status: "all", q: "", currentId: null, mode: null }; /* ---------- утилиты ---------- */ function $(id) { return document.getElementById(id); } var ESC_MAP = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return ESC_MAP[c]; }); } function byId(id) { for (var i = 0; i < S.items.length; i++) if (S.items[i].id === id) return S.items[i]; return null; } function nextId() { var m = 1; for (var i = 0; i < S.items.length; i++) if (S.items[i].id >= m) m = S.items[i].id + 1; return m; } function statusOf(id) { for (var i = 0; i < STATUSES.length; i++) if (STATUSES[i].id === id) return STATUSES[i]; return null; } function addLog(user, what) { S.log.unshift({ when: new Date().toISOString(), user: user, what: what }); if (S.log.length > 100) S.log.length = 100; persist(); } function icon(name) { return ''; } function toast(msg, tone) { var stack = $("toasts"); if (!stack) return; var d = document.createElement("div"); d.className = "kt-ai-toast"; d.setAttribute("data-tone", tone || "ok"); d.textContent = msg; stack.appendChild(d); setTimeout(function () { if (d.parentNode) d.parentNode.removeChild(d); }, 3200); } /* ---------- форма ---------- */ var FIELD_TITLES = { person: "Кто подал", what: "Что сломалось", deadline: "Срок ремонта", status: "Статус", note: "Комментарий" }; function formHtml(mode) { var r = state.currentId ? byId(state.currentId) : null; var isNew = !r; var p = r ? r.person : ""; var w = r ? r.what : ""; var today = dstr(0); var dISO = r ? r.deadline : dstr(7); var st = r ? r.status : "new"; var note = r ? r.note : ""; var h = '
' + '
' + '' + '" + '' + (isNew ? "" : '") + '" + "" + "
"; return h; } function statusOptions(sel) { var h = ""; for (var i = 0; i < STATUSES.length; i++) { var s = STATUSES[i]; h += '"; } return h; } /* ---------- ящик ---------- */ function openDrawer() { $("drawer").hidden = false; $("drawerOverlay").hidden = false; document.body.style.overflow = "hidden"; var closeBtn = $("drawerClose"); if (closeBtn) closeBtn.focus(); } function closeDrawer() { $("drawer").hidden = true; $("drawerOverlay").hidden = true; document.body.style.overflow = ""; state.mode = null; state.currentId = null; } function openDetail(id) { var r = byId(id); if (!r) return; state.currentId = id; state.mode = "view"; openDrawer(); renderDetail(r); } function propLine(k, v) { return '
' + k + "" + v + "
"; } function renderDetail(r) { var st = statusOf(r.status); var actions = ""; $("drawerTitle").textContent = "Заявка №" + r.id; $("drawerMeta").textContent = "подана " + fmtDate(r.submitted); var events = (r.events || []).slice().reverse(); var evHtml = ""; for (var i = 0; i < events.length && i < 8; i++) { var e = events[i]; evHtml += '
' + esc(fmtWhen(e.when)) + '' + esc(e.user) + '' + esc(e.what) + "
"; } $("drawerBody").innerHTML = '
' + propLine(FIELD_TITLES.person, esc(r.person)) + propLine(FIELD_TITLES.what, "" + esc(r.what) + "") + propLine(FIELD_TITLES.deadline, "" + esc(deadlineLabel(r)) + "") + propLine(FIELD_TITLES.status, '' + esc(st.label) + "") + (r.note ? '
' + FIELD_TITLES.note + "

" + esc(r.note) + "

" : "") + '
История' + (events.length ? '
' + evHtml + "
" : '

Изменений не записано

') + "
"; actions = '' + '' + '" + '"; renderActions(actions); } function renderForm(r) { var isNew = !r; $("drawerTitle").textContent = isNew ? "Новая заявка" : "Изменить заявку №" + r.id; $("drawerMeta").textContent = isNew ? "" : "подана " + fmtDate(r.submitted) + " · можно изменить любой параметр"; $("drawerBody").innerHTML = formHtml(state.mode); var actions = isNew ? '" + '' + '' : '' + '' + '"; renderActions(actions); bindForm(); var first = $("fPerson"); if (first) first.focus(); } function bindForm() { var f = document.getElementById("zform"); if (!f) return; f.addEventListener("submit", function (e) { e.preventDefault(); saveForm(); }); } function renderActions(html) { var bar = $("drawerActions"); bar.innerHTML = html; var btns = bar.querySelectorAll("[data-act]"); for (var i = 0; i < btns.length; i++) btns[i].addEventListener("click", onDrawerAction); } function onDrawerAction(e) { var act = e.currentTarget.getAttribute("data-act"); if (act === "close") { closeDrawer(); return; } if (act === "cancel") { if (state.mode === "create") { closeDrawer(); return; } state.mode = "view"; renderDetail(byId(state.currentId)); return; } if (act === "edit") { state.mode = "edit"; renderForm(byId(state.currentId)); return; } if (act === "delete") { deleteAction(state.currentId); return; } } function deleteAction(id) { var r = byId(id); if (!r) return; if (!confirm("Удалить заявку №" + id + " («" + r.what + "»)?")) return; S.items = S.items.filter(function (x) { return x.id !== id; }); addLog("Служба", "Заявка №" + id + " удалена"); persist(); renderAll(); closeDrawer(); toast("Заявка удалена", "ok"); } function saveForm() { var p = (document.getElementById("fPerson") || {}).value || ""; var w = (document.getElementById("fWhat") || {}).value || ""; var dISO = (document.getElementById("fDeadline") || {}).value || ""; var stEl = document.getElementById("fStatus"); var st = stEl ? stEl.value : "new"; var note = (document.getElementById("fNote") || {}).value || ""; p = p.trim(); w = w.trim(); note = note.trim(); var today = dstr(0); var err = ""; if (!p) err = "Укажите, кто подал"; else if (!w) err = "Опишите, что сломалось"; else if (!dISO) err = "Укажите срок"; else if (dISO < today) err = "Срок не может быть раньше сегодняшнего дня"; if (err) { var el = document.getElementById("formErr"); if (el) { el.textContent = err; el.hidden = false; } return; } var user = p; var r = state.currentId ? byId(state.currentId) : null; if (state.mode === "create") { var rec = { id: nextId(), person: p, what: w, deadline: dISO, status: "new", submitted: today, note: note, events: [{ when: new Date().toISOString(), user: p, what: "заявка подана" }] }; S.items.push(rec); addLog(user, "Заявка №" + rec.id + " подана"); persist(); renderAll(); toast("Заявка №" + rec.id + " подана", "ok"); state.currentId = rec.id; state.mode = "view"; renderDetail(rec); return; } if (r) { var changes = []; if (r.person !== p) changes.push(FIELD_TITLES.person); if (r.what !== w) changes.push(FIELD_TITLES.what); if (r.deadline !== dISO) changes.push(FIELD_TITLES.deadline); if (r.status !== st) changes.push(FIELD_TITLES.status); if (r.note !== note) changes.push(FIELD_TITLES.note); if (changes.length) { r.person = p; r.what = w; r.deadline = dISO; r.status = st; r.note = note; changeLog(r, p, changes); persist(); renderAll(); toast("Заявка №" + r.id + " сохранена", "ok"); state.mode = "view"; renderDetail(r); return; } toast("Ничего не изменилось", "info"); } } function changeLog(r, who, fields) { var names = fields.slice(); if (fields.indexOf(FIELD_TITLES.status) !== -1) names[names.indexOf(FIELD_TITLES.status)] += " → " + statusOf(r.status).label; var label = names.join(", "); addLog(who, "Заявка №" + r.id + " изменена: " + label); (r.events = r.events || []).push({ when: new Date().toISOString(), user: who, what: label }); } function newForm() { state.currentId = null; state.mode = "create"; openDrawer(); renderForm(null); } /* ---------- выборка ---------- */ function listVisible() { var list = S.items.slice(); if (state.status !== "all") list = list.filter(function (r) { return r.status === state.status; }); var q = state.q.trim().toLowerCase(); if (q) list = list.filter(function (r) { return (r.person + " " + r.what).toLowerCase().indexOf(q) !== -1; }); list.sort(function (a, b) { if (a.deadline !== b.deadline) return a.deadline < b.deadline ? -1 : 1; return a.id < b.id ? -1 : 1; }); return list; } function counts() { var c = { all: S.items.length }; for (var i = 0; i < S.items.length; i++) c[S.items[i].status] = (c[S.items[i].status] || 0) + 1; return c; } /* ---------- рендер ---------- */ function renderAll() { renderKPI(); renderChips(); renderTable(); renderLogSection(); } function renderKPI() { var open = 0, wait = 0, over = 0; var today = dstr(0); for (var i = 0; i < S.items.length; i++) { var r = S.items[i]; if (r.status === "done") continue; open++; if (r.status === "wait") wait++; if (r.deadline && r.deadline < today) over++; } $("kpiStrip").innerHTML = '
Всего' + S.items.length + "
" + '
Открыто' + open + 'в очереди
' + '
Ждут запчасть' + wait + "
" + '
Просрочено' + over + "
"; } function renderChips() { var c = counts(); var h = chipBtn("all", "Все", c.all || 0, state.status === "all"); for (var i = 0; i < STATUSES.length; i++) { h += chipBtn(STATUSES[i].id, STATUSES[i].label, c[STATUSES[i].id] || 0, state.status === STATUSES[i].id); } $("filterChips").innerHTML = h; var btns = $("filterChips").querySelectorAll("button"); for (var j = 0; j < btns.length; j++) btns[j].addEventListener("click", onChip); } function chipBtn(id, label, n, active) { return '"; } function onChip(e) { state.status = e.currentTarget.getAttribute("data-id"); renderChips(); renderTable(); } function renderTable() { var list = listVisible(); var h = ""; for (var i = 0; i < list.length; i++) h += rowHtml(list[i]); $("rowsBody").innerHTML = h; var foot = document.getElementById("tableFoot"); if (foot) foot.textContent = "Показано " + list.length + (list.length === 1 ? "" : list.length < 5 ? " строки" : " строк"); var empty = list.length === 0; $("listView").hidden = empty; $("emptyState").hidden = !empty; if (empty) { $("emptyState").innerHTML = emptyHtml(); var b = $("emptyState").querySelector("button"); if (b) b.addEventListener("click", emptyAction); } else { var trs = $("rowsBody").querySelectorAll("tr"); for (var k = 0; k < trs.length; k++) bindRow(trs[k]); } renderConclusion(list); } function rowHtml(r) { var st = statusOf(r.status); var today = dstr(0); var ov = r.status !== "done" && r.deadline && r.deadline < today; return '' + "" + esc(r.person) + "" + "" + esc(r.what) + "" + "" + esc(deadlineLabel(r)) + "" + "" + '' + esc(st.label) + ""; } function bindRow(tr) { tr.addEventListener("click", function () { openDetail(parseInt(tr.getAttribute("data-id"), 10)); }); tr.addEventListener("keydown", function (e) { if (e.key === "Enter") openDetail(parseInt(e.currentTarget.getAttribute("data-id"), 10)); }); } function emptyHtml() { if (state.q || state.status !== "all") { return '
' + icon("filter") + '

Ничего не найдено

' + '

Измените фильтр или поиск, чтобы увидеть заявки

' + '
'; } return '
' + icon("inbox") + '

Заявок пока нет

' + '

Добавьте первую запись из журнала или по кнопке «Новая заявка»

' + '
'; } function emptyAction() { if (state.q || state.status !== "all") { state.status = "all"; state.q = ""; $("searchInput").value = ""; renderChips(); renderTable(); renderKPI(); return; } newForm(); } function renderConclusion(list) { var over = 0, soon = 0; for (var i = 0; i < list.length; i++) { var r = list[i]; if (r.status === "done" || !r.deadline) continue; var df = daysDiff(r.deadline); if (df < 0) over++; else if (df <= 2) soon++; } var head = "В выборке " + list.length + (list.length === 1 ? " заявка" : list.length < 5 ? " заявки" : " заявок"); var st = statusOf(state.status); if (state.status !== "all" && st) head += ", фильтр «" + st.label + "»"; if (state.q) head += ", поиск «" + state.q + "»"; var tail; if (over === 0 && soon === 0) tail = " Просрочек нет, ближайший срок не ближе двух дней."; else { tail = over > 0 ? " Просрочено: " + over + "." : ""; tail += soon > 0 ? " Сроки в ближайшие 2 дня: " + soon + "." : ""; } $("conclusion").innerHTML = 'Сводка' + esc(head + tail) + ""; } var LOG_LIMIT = 12; function renderLogSection() { var body = $("logBody"); if (!body) return; var n = Math.min(S.log.length, LOG_LIMIT); var h = ""; for (var i = 0; i < n; i++) { var l = S.log[i]; h += "" + '' + esc(fmtWhen(l.when)) + "" + "" + esc(l.user) + "" + "" + esc(l.what) + (i === 0 ? ' — первое' : "") + ""; } body.innerHTML = h; if (n === 0) body.innerHTML = 'Записей пока нет'; } /* ---------- CSV-выгрузка ---------- */ function exportCsv() { var rows = listVisible(); function cell(v) { return '"' + String(v == null ? "" : v).replace(/"/g, '""') + '"'; } var out = ["№;Кто подал;Что сломалось;Срок;Статус;Подана"]; for (var i = 0; i < rows.length; i++) { var r = rows[i]; out.push([r.id, r.person, r.what, fmtDate(r.deadline), (statusOf(r.status) || {}).label || r.status, fmtDate(r.submitted)].map(cell).join(";")); } var blob = new Blob(["\uFEFF" + out.join("\r\n")], { type: "text/csv;charset=utf-8" }); var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "journal-" + dstr(0) + ".csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000); toast("Выгружено: " + rows.length + " (CSV)", "ok"); } /* ---------- тема ---------- */ function applyTheme(mode) { document.documentElement.setAttribute("data-theme", mode); var t = document.getElementById("themeToggle"); if (t) { t.setAttribute("data-mode", mode); t.setAttribute("aria-label", mode === "dark" ? "Светлая тема" : "Тёмная тема"); } try { localStorage.setItem("remont-theme", mode); } catch (err) {} } function initTheme() { var saved = "light"; try { saved = localStorage.getItem("remont-theme") || "light"; } catch (err) {} applyTheme(saved); } /* ---------- init ---------- */ function init() { initTheme(); var today = document.getElementById("todayChip"); if (today) today.textContent = fmtDate(dstr(0)); renderAll(); bindApp(); } function bindApp() { var btnNew = document.getElementById("btnNew"); if (btnNew) btnNew.addEventListener("click", newForm); var drawerClose = document.getElementById("drawerClose"); if (drawerClose) drawerClose.addEventListener("click", closeDrawer); var drawerOverlay = document.getElementById("drawerOverlay"); if (drawerOverlay) drawerOverlay.addEventListener("click", closeDrawer); var themeToggle = document.getElementById("themeToggle"); if (themeToggle) themeToggle.addEventListener("click", function () { applyTheme(document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark"); }); var btnCsv = document.getElementById("btnCsv"); if (btnCsv) btnCsv.addEventListener("click", exportCsv); var btnReset = document.getElementById("btnReset"); if (btnReset) btnReset.addEventListener("click", function () { if (!confirm("Вернуть журнал к исходным данным? Внесённые изменения будут потеряны.")) return; resetData(); }); var searchInput = document.getElementById("searchInput"); if (searchInput) searchInput.addEventListener("input", function () { state.q = this.value; renderTable(); renderKPI(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { var drawer = document.getElementById("drawer"); if (drawer && !drawer.hidden) closeDrawer(); } }); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); })();