zhurnal-zayavok-na-remont-ob-3/script.js

682 lines
31 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Журнал заявок на ремонт оборудования. Данные в 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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
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 '<svg class="kt-icon" aria-hidden="true"><use href="' + ICON_BASE + "#" + name + '"></use></svg>';
}
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 = '<div id="zformRoot">' +
'<form id="zform" novalidate>' +
'<label class="kt-ai-field"><span class="cap">' + FIELD_TITLES.person + " <span class=\"req\">*</span></span>" +
'<input class="kt-ai-input" id="fPerson" value="' + esc(p) + '" maxlength="80" placeholder="ФИО сотрудника" autocomplete="off"></label>' +
'<label class="kt-ai-field"><span class="cap">' + FIELD_TITLES.what + " <span class=\"req\">*</span></span>" +
'<textarea class="kt-ai-textarea" id="fWhat" rows="4" maxlength="400" placeholder="Что, где, какой симптом">' + esc(w) + "</textarea></label>" +
'<label class="kt-ai-field"><span class="cap">' + FIELD_TITLES.deadline + " <span class=\"req\">*</span></span>" +
'<input class="kt-ai-input" id="fDeadline" type="date" value="' + dISO + '" min="' + today + '"></label>' +
(isNew ? "" :
'<label class="kt-ai-field"><span class="cap">' + FIELD_TITLES.status + "</span>" +
'<select class="kt-ai-select" id="fStatus">' + statusOptions(st) + "</select></label>") +
'<label class="kt-ai-field"><span class="cap">' + FIELD_TITLES.note + "</span>" +
'<textarea class="kt-ai-textarea" id="fNote" rows="3" maxlength="400" placeholder="Дополнительно">' + esc(note) + "</textarea></label>" +
"<div class=\"form-err\" id=\"formErr\" hidden></div>" +
"</form></div>";
return h;
}
function statusOptions(sel) {
var h = "";
for (var i = 0; i < STATUSES.length; i++) {
var s = STATUSES[i];
h += '<option value="' + s.id + '"' + (s.id === sel ? " selected" : "") + ">" + esc(s.label) + "</option>";
}
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 '<div class="kt-ai-prop"><span class="k">' + k + "</span><span class=\"v\">" + v + "</span></div>"; }
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 += '<div class="event-row"><span class="event-time kt-ai-num">' + esc(fmtWhen(e.when)) +
'</span><span class="event-main"><span class="event-user kt-ai-meta">' + esc(e.user) +
'</span><span class="event-what">' + esc(e.what) + "</span></span></div>";
}
$("drawerBody").innerHTML =
'<div class="detail-stack">' +
propLine(FIELD_TITLES.person, esc(r.person)) +
propLine(FIELD_TITLES.what, "<span>" + esc(r.what) + "</span>") +
propLine(FIELD_TITLES.deadline, "<span>" + esc(deadlineLabel(r)) + "</span>") +
propLine(FIELD_TITLES.status, '<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span>") +
(r.note ? '<div class="detail-note"><span class="kt-ai-label">' + FIELD_TITLES.note + "</span><p class=\"note-text\">" + esc(r.note) + "</p></div>" : "") +
'<div class="detail-note"><span class="kt-ai-label">История</span>' +
(events.length ? '<div class="event-list">' + evHtml + "</div>" : '<p class="kt-ai-meta">Изменений не записано</p>') +
"</div>";
actions =
'<button class="kt-ai-btn" data-act="close">Закрыть</button>' +
'<span class="spacer"></span>' +
'<button class="kt-ai-btn" data-act="edit">' + icon("pencil") + " Изменить</button>" +
'<button class="kt-ai-btn" data-variant="danger" data-act="delete">' + icon("trash") + " Удалить</button>";
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
? '<button class="kt-ai-btn" data-act="save" data-variant="primary" type="submit" form="zform">' + icon("check") + " Подать заявку</button>" +
'<span class="spacer"></span>' +
'<button class="kt-ai-btn" data-act="cancel">Отмена</button>'
: '<button class="kt-ai-btn" data-act="cancel">Закрыть</button>' +
'<span class="spacer"></span>' +
'<button class="kt-ai-btn" data-variant="primary" data-act="save" type="submit" form="zform">' + icon("check") + " Сохранить</button>";
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 =
'<div class="kt-ai-kpi"><span class="label">Всего</span><span class="value kt-ai-num">' + S.items.length + "</span></div>" +
'<div class="kt-ai-kpi"><span class="label">Открыто</span><span class="value kt-ai-num">' + open + '</span><span class="hint">в очереди</span></div>' +
'<div class="kt-ai-kpi"><span class="label">Ждут запчасть</span><span class="value kt-ai-num">' + wait + "</span></div>" +
'<div class="kt-ai-kpi"><span class="label">Просрочено</span><span class="value kt-ai-num">' + over + "</span></div>";
}
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 '<button type="button" class="kt-ai-filter-chip" data-id="' + id + '"' +
(active ? ' data-active="true"' : "") + ">" + esc(label) +
' <span class="count kt-ai-num">' + n + "</span></button>";
}
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 '<tr data-id="' + r.id + '" class="' + (ov ? "is-overdue" : "") + '">' +
"<td>" + esc(r.person) + "</td>" +
"<td>" + esc(r.what) + "</td>" +
"<td>" + esc(deadlineLabel(r)) + "</td>" +
"<td>" +
'<span class="kt-ai-status-pill" data-status="' + st.tone + '">' + esc(st.label) + "</span></td></tr>";
}
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 '<div class="empty-stack">' + icon("filter") +
'<p class="title">Ничего не найдено</p>' +
'<p class="kt-ai-sub">Измените фильтр или поиск, чтобы увидеть заявки</p>' +
'<button type="button" class="kt-ai-btn" id="emptyReset">Сбросить фильтр</button></div>';
}
return '<div class="empty-stack">' + icon("inbox") +
'<p class="title">Заявок пока нет</p>' +
'<p class="kt-ai-sub">Добавьте первую запись из журнала или по кнопке «Новая заявка»</p>' +
'<button type="button" class="kt-ai-btn" id="emptyNew">Подать заявку</button></div>';
}
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 = '<span class="title">Сводка</span><span class="text">' + esc(head + tail) + "</span>";
}
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 += "<tr>" +
'<td class="kt-ai-num">' + esc(fmtWhen(l.when)) + "</td>" +
"<td>" + esc(l.user) + "</td>" +
"<td>" + esc(l.what) +
(i === 0 ? ' <span class="log-dash">— первое</span>' : "") +
"</td></tr>";
}
body.innerHTML = h;
if (n === 0) body.innerHTML = '<tr><td colspan="3" class="kt-ai-meta">Записей пока нет</td></tr>';
}
/* ---------- 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();
})();