682 lines
31 KiB
JavaScript
682 lines
31 KiB
JavaScript
/* Журнал заявок на ремонт оборудования. Данные в 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 '<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();
|
||
})();
|