552 lines
22 KiB
JavaScript
552 lines
22 KiB
JavaScript
// Журнал заявок на ремонт оборудования.
|
||
// Данные хранятся в 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, ">")
|
||
.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 '<span class="kt-ai-status-pill" data-status="' + esc(tone) + '">' +
|
||
esc(STATUS_LABEL[status] || status) + "</span>";
|
||
}
|
||
|
||
// ————————————————————————————————
|
||
// Рендер
|
||
// ————————————————————————————————
|
||
|
||
function renderFilters() {
|
||
var c = countByStatus();
|
||
var host = $("statusFilters");
|
||
if (!host) return;
|
||
var parts = [];
|
||
parts.push('<button type="button" class="status-chip" data-status="all" data-active="' + (state.filter === "all") + '">Все <span class="count">' + state.reqs.length + "</span></button>");
|
||
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('<button type="button" class="status-chip" data-status="' + esc(s) + '" data-active="' + active + '">' + esc(label) + ' <span class="count">' + esc(String(c[s])) + "</span></button>");
|
||
}
|
||
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 '<tr class="selectable" data-id="' + esc(r.id) + '" data-selected="' + selected + '">' +
|
||
'<td data-label="Заявка"><div class="req-title"><span class="id">' + esc(r.id) + "</span>" +
|
||
'<span class="req-sub">' + esc(r.equipment || "") + "</span></div></td>" +
|
||
"<td>" + pillHtml(r.status) + "</td>" +
|
||
'<td><span class="kt-ai-chip">' + cls + "</span></td>" +
|
||
'<td><span class="cell-date">' + fmtDate(r.created) + "</span></td>" +
|
||
"</tr>";
|
||
}
|
||
|
||
function renderDetail() {
|
||
var host = $("detailPane");
|
||
if (!host) return;
|
||
var r = state.selectedId ? findReq(state.selectedId) : null;
|
||
if (!r) {
|
||
host.innerHTML =
|
||
'<div class="detail-hint">' +
|
||
'<svg class="kt-icon" aria-hidden="true"><use href="#clipboard"></use></svg>' +
|
||
'<div><strong>Выберите заявку</strong></div>' +
|
||
'<div class="kt-ai-meta">Нажмите на строку в списке, чтобы увидеть детали и сменить статус.</div>' +
|
||
"</div>";
|
||
return;
|
||
}
|
||
var resp = r.resp ? esc(r.resp) : "—";
|
||
var cls = esc(r.cls || "—");
|
||
host.innerHTML =
|
||
'<div class="detail-card">' +
|
||
'<div class="detail-head">' +
|
||
'<div class="id">' + esc(r.id) + "</div>" +
|
||
'<div class="detail-title-row">' +
|
||
'<div class="detail-title">' + esc(r.equipment || "") + "</div>" +
|
||
pillHtml(r.status) +
|
||
"</div>" +
|
||
"</div>" +
|
||
'<div class="detail-rows">' +
|
||
detailRow("Место", r.place) +
|
||
detailRow("Класс", cls) +
|
||
detailRow("Создана", fmtDate(r.created)) +
|
||
detailRow("Обновлена", fmtDate(r.updated)) +
|
||
detailRow("Исполнитель", resp) +
|
||
"</div>" +
|
||
'<div class="kt-ai-meta desc-label">Описание</div>' +
|
||
'<p class="detail-desc">' + (r.desc ? esc(r.desc) : "—") + "</p>" +
|
||
'<div class="detail-status-line"><label class="kt-ai-meta" for="statusSelect">Статус</label></div>' +
|
||
'<select class="kt-ai-select" id="statusSelect" style="margin-top:var(--kt-ai-space-2)">' + statusOptions(r.status) + "</select>" +
|
||
"</div>";
|
||
var sel = $("statusSelect");
|
||
if (sel) {
|
||
sel.addEventListener("change", onStatusChange);
|
||
}
|
||
}
|
||
|
||
function detailRow(k, v) {
|
||
return '<div class="detail-row"><span class="k">' + esc(k) + '</span><span class="v">' + esc(v) + "</span></div>";
|
||
}
|
||
|
||
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('<option value="' + esc(s) + '" ' + selected + "'>" + esc(STATUS_LABEL[s]) + "</option>");
|
||
}
|
||
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();
|
||
}
|