270 lines
11 KiB
JavaScript
270 lines
11 KiB
JavaScript
/* Учёт заявок на пропуск — демо: хранение в localStorage.
|
||
Демо-набор отдельно (key "kt_pass_demo_seed") от добавленных пользователем записей
|
||
(key "kt_pass_requests") — показываем как есть, без смешивания и подмены. */
|
||
|
||
var STATUS = {
|
||
new: { label: "Новая", pill: "info" },
|
||
processing: { label: "В обработке", pill: "warn" },
|
||
approved: { label: "Одобрена", pill: "ok" },
|
||
rejected: { label: "Отклонена", pill: "risk" }
|
||
};
|
||
var STATUS_ORDER = ["new", "processing", "approved", "rejected"];
|
||
|
||
var DEMO = [
|
||
{ id: 1042, name: "Ахметов Ерлан Сергеевич", purpose: "Въезд на технологическую территорию ЦОД Алматы", date: "2026-09-18" },
|
||
{ id: 1041, name: "Бекеева Алия Нурлановна", purpose: "Обучение: работа с ОДЗ, учебный класс B", date: "2026-09-17" },
|
||
{ id: 1040, name: "Сериков Данияр Олегович", purpose: "Работа подрядчика, зона монтажа №3", date: "2026-09-16" },
|
||
{ id: 1039, name: "Махамбетова Гульнара", purpose: "Ревизия склада СИЗ", date: "2026-09-15" },
|
||
{ id: 1038, name: "Токтаров Нурлан", purpose: "Въезд на территорию офиса №2", date: "2026-09-15" },
|
||
{ id: 1037, name: "Абдрахманова Семина", purpose: "Курсы ИБ, 2 день", date: "2026-09-12" },
|
||
{ id: 1036, name: "Куанышбаев Асхат", purpose: "Сопровождение инспекции", date: "2026-09-12" },
|
||
{ id: 1035, name: "Омарова Жания", purpose: "Подача документов в отдел кадров", date: "2026-09-10" },
|
||
{ id: 1034, name: "Нурланов Серик", purpose: "Доставка оборудования, грузовой въезд", date: "2026-09-05" }
|
||
];
|
||
var DEMO_STATUS = { 1042: "new", 1041: "new", 1040: "processing", 1039: "processing", 1038: "approved", 1037: "approved", 1036: "rejected", 1035: "rejected", 1034: "approved" };
|
||
|
||
/** Демо-набор: статусы приложены и зафиксированы, перезаписывать нельзя. */
|
||
function demoItems() {
|
||
return DEMO.map(function (d) {
|
||
return { id: d.id, name: d.name, purpose: d.purpose, date: d.date, status: DEMO_STATUS[d.id], demo: true };
|
||
});
|
||
}
|
||
|
||
/** Записи, добавленные пользователем в этой сессии браузера. */
|
||
function loadUser() {
|
||
try {
|
||
var raw = localStorage.getItem("kt_pass_requests");
|
||
var arr = raw ? JSON.parse(raw) : [];
|
||
return Array.isArray(arr) ? arr.filter(function (r) { return isReq(r); }) : [];
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
function saveUser(arr) {
|
||
try {
|
||
localStorage.setItem("kt_pass_requests", JSON.stringify(arr));
|
||
} catch (e) {
|
||
/* localStorage недоступен — работаем только в памяти */
|
||
}
|
||
}
|
||
function isReq(r) {
|
||
return r && r.name && r.purpose && r.date && STATUS[r.status] != null && r.id > 0;
|
||
}
|
||
function nextId(user) {
|
||
var m = 0;
|
||
user.forEach(function (r) { if (r.id > m) m = r.id; });
|
||
return 10000 + user.length + 1 + (m >= 10000 ? m - 9999 : 0);
|
||
}
|
||
|
||
var state = { filter: "all", user: loadUser() };
|
||
|
||
var $form = document.getElementById("request-form");
|
||
var $kpiStrip = document.getElementById("kpi-strip");
|
||
var $tableWrap = document.getElementById("table-wrap");
|
||
var $tbody = document.getElementById("list-body");
|
||
var $empty = document.getElementById("list-empty");
|
||
var $emptyHint = document.getElementById("list-empty-hint");
|
||
var $total = document.getElementById("list-total");
|
||
var $toastStack = document.getElementById("toast-stack");
|
||
|
||
function esc(s) {
|
||
return String(s).replace(/[&<>"']/g, function (c) {
|
||
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
||
});
|
||
}
|
||
function pad(n) { return String(n).length < 2 ? "0" + n : String(n); }
|
||
function fmtDate(iso) {
|
||
if (!iso) return "—";
|
||
var parts = iso.split("-");
|
||
if (parts.length !== 3) return iso;
|
||
return parts[2] + "." + parts[1] + "." + parts[0];
|
||
}
|
||
function statusInfo(s) {
|
||
return STATUS[s] || STATUS.new;
|
||
}
|
||
|
||
function allItems() {
|
||
var merged = state.user.map(function (r) {
|
||
return { id: r.id, name: r.name, purpose: r.purpose, date: r.date, status: r.status, demo: false };
|
||
}).concat(demoItems());
|
||
merged.sort(function (a, b) { return b.id - a.id; });
|
||
return merged;
|
||
}
|
||
function visible() {
|
||
var all = allItems();
|
||
if (state.filter === "all") return all;
|
||
return all.filter(function (r) { return r.status === state.filter; });
|
||
}
|
||
|
||
function renderKPI() {
|
||
var all = allItems();
|
||
var counts = { all: all.length };
|
||
STATUS_ORDER.forEach(function (k) { counts[k] = 0; });
|
||
all.forEach(function (r) { counts[r.status] = (counts[r.status] || 0) + 1; });
|
||
|
||
function kpi(id, value, label) {
|
||
var active = state.filter === id ? "true" : "false";
|
||
return '<div class="kt-ai-kpi" data-tab="true" data-active="' + active + '" data-status="' + id + '">' +
|
||
'<span class="value">' + value + "</span>" +
|
||
'<span class="label">' + label + "</span>" +
|
||
"</div>";
|
||
}
|
||
var KPIS = [
|
||
["all", "Всего"],
|
||
["new", "Новые"],
|
||
["processing", "В обработке"],
|
||
["approved", "Одобрено"],
|
||
["rejected", "Отклонено"]
|
||
];
|
||
var parts = KPIS.map(function (k) { return kpi(k[0], counts[k[0]], k[1]); });
|
||
$kpiStrip.innerHTML = parts.join("");
|
||
}
|
||
|
||
function plural(n, one, few, many) {
|
||
var n10 = n % 10, n100 = n % 100;
|
||
if (n10 === 1 && n100 !== 11) return one;
|
||
if (n10 >= 2 && n10 <= 4 && (n100 < 12 || n100 > 14)) return few;
|
||
return many;
|
||
}
|
||
|
||
function renderList() {
|
||
var list = visible();
|
||
var total = allItems().length;
|
||
$total.textContent = total + " " + plural(total, "заявка", "заявки", "заявок");
|
||
|
||
if (list.length === 0) {
|
||
$tbody.innerHTML = "";
|
||
$tableWrap.hidden = true;
|
||
$empty.hidden = false;
|
||
$emptyHint.textContent = state.filter === "all"
|
||
? "Заявок пока нет — создайте первую в форме выше."
|
||
: "Под выбранный статус заявки не нашлись.";
|
||
return;
|
||
}
|
||
$tableWrap.hidden = false;
|
||
$empty.hidden = true;
|
||
|
||
var rows = list.map(function (r) {
|
||
var st = statusInfo(r.status);
|
||
var actions = "";
|
||
if (!r.demo) {
|
||
if (r.status === "new") {
|
||
actions += btnAction(r.id, "processing", "check", "В обработку");
|
||
} else if (r.status === "approved") {
|
||
actions += btnAction(r.id, "new", "refresh", "Сменить статус");
|
||
}
|
||
actions += btnAction(r.id, "delete", "trash", "Удалить", "ghost");
|
||
}
|
||
return "<tr>" +
|
||
'<td class="num">' + r.id + "</td>" +
|
||
"<td>" + esc(r.name) + "</td>" +
|
||
'<td class="kt-ai-truncate" title="' + esc(r.purpose) + '">' + esc(r.purpose) + "</td>" +
|
||
'<td class="num">' + fmtDate(r.date) + "</td>" +
|
||
'<td class="t-status"><span class="kt-ai-chip" data-tone="' + st.pill + '">' + st.label + "</span></td>" +
|
||
'<td class="t-col-actions"><span class="t-actions">' + (actions || "—") + "</span></td>" +
|
||
"</tr>";
|
||
});
|
||
$tbody.innerHTML = rows.join("");
|
||
}
|
||
|
||
function btnAction(id, target, icon, label, variant) {
|
||
return '<button class="kt-ai-btn" type="button" data-action="' + target + '" data-id="' + id + '"' +
|
||
(variant ? ' data-variant="' + variant + '"' : "") + ' title="' + label + '">' +
|
||
'<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#' + icon + '"></use></svg>' +
|
||
(label !== "Удалить" ? "<span>" + label + "</span>" : "") +
|
||
"</button>";
|
||
}
|
||
|
||
function render() {
|
||
renderKPI();
|
||
renderList();
|
||
}
|
||
|
||
function toast(text, tone) {
|
||
var el = document.createElement("div");
|
||
el.className = "kt-ai-toast";
|
||
if (tone) el.setAttribute("data-tone", tone);
|
||
el.textContent = text;
|
||
$toastStack.appendChild(el);
|
||
setTimeout(function () { el.remove(); }, 4000);
|
||
}
|
||
|
||
function setInvalid(input, bad) {
|
||
input.setAttribute("data-invalid", bad ? "true" : "false");
|
||
var field = input.closest(".kt-ai-field");
|
||
if (field) {
|
||
var err = field.querySelector(".error");
|
||
if (err) err.hidden = !bad;
|
||
field.setAttribute("data-invalid", bad ? "true" : "false");
|
||
}
|
||
}
|
||
|
||
function setFilter(s) {
|
||
state.filter = s;
|
||
document.querySelectorAll(".kt-ai-filter-chip[data-status]").forEach(function (c) {
|
||
c.setAttribute("data-active", c.getAttribute("data-status") === s ? "true" : "false");
|
||
});
|
||
renderKPI();
|
||
renderList();
|
||
}
|
||
|
||
document.addEventListener("click", function (e) {
|
||
var chip = e.target.closest(".kt-ai-filter-chip[data-status]");
|
||
if (chip) { setFilter(chip.getAttribute("data-status")); return; }
|
||
|
||
var kpi = e.target.closest(".kt-ai-kpi[data-status]");
|
||
if (kpi) { setFilter(kpi.getAttribute("data-status")); return; }
|
||
|
||
var act = e.target.closest(".kt-ai-btn[data-action]");
|
||
if (act) {
|
||
var id = Number(act.getAttribute("data-id"));
|
||
var target = act.getAttribute("data-action");
|
||
state.user = state.user.filter(function (r) {
|
||
if (r.id !== id) return true;
|
||
if (target === "delete") { toast("Заявка №" + id + " удалена"); return false; }
|
||
r.status = target;
|
||
toast(
|
||
target === "processing" ? "Заявка №" + id + " перенесена в обработку" :
|
||
target === "approved" ? "Заявка №" + id + " одобрена" :
|
||
"Статус заявки №" + id + ": " + statusInfo(target).label
|
||
);
|
||
return true;
|
||
});
|
||
saveUser(state.user);
|
||
render();
|
||
}
|
||
});
|
||
|
||
document.getElementById("btn-reset").addEventListener("click", function () { setFilter("all"); });
|
||
|
||
["f-name", "f-purpose", "f-date"].forEach(function (i) {
|
||
document.getElementById(i).addEventListener("input", function () { setInvalid(this, false); });
|
||
});
|
||
|
||
$form.addEventListener("submit", function (e) {
|
||
e.preventDefault();
|
||
var f = $form;
|
||
var name = f.elements["f-name"].value.trim();
|
||
var purpose = f.elements["f-purpose"].value.trim();
|
||
var date = f.elements["f-date"].value;
|
||
var ok = true;
|
||
if (!name) { setInvalid(f.elements["f-name"], true); ok = false; }
|
||
if (!purpose) { setInvalid(f.elements["f-purpose"], true); ok = false; }
|
||
if (!date) { setInvalid(f.elements["f-date"], true); ok = false; }
|
||
if (!ok) { toast("Проверьте поля формы", "warn"); return; }
|
||
|
||
var item = { id: nextId(state.user), name: name, purpose: purpose, date: date, status: "new" };
|
||
state.user.push(item);
|
||
saveUser(state.user);
|
||
f.reset();
|
||
render();
|
||
toast("Заявка №" + item.id + " создана, статус: Новая");
|
||
$form.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||
});
|
||
|
||
(function init() {
|
||
var today = new Date();
|
||
document.getElementById("f-date").valueAsDate = today;
|
||
render();
|
||
})();
|