From a53446e0ad48c26219d939183ba94ca49d518062 Mon Sep 17 00:00:00 2001 From: Altynbek Date: Fri, 14 Aug 2026 06:46:17 +0000 Subject: [PATCH] =?UTF-8?q?v2:=20=D1=80=D0=B5=D0=BC=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20DSLAM=20=D0=B1=D0=B8=D0=BB=D0=B5?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 436 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 305 insertions(+), 131 deletions(-) diff --git a/index.html b/index.html index f369b5b..729cdba 100644 --- a/index.html +++ b/index.html @@ -3,18 +3,27 @@ - Заявки на повреждение СТПК + Ремедиация DSLAM — Проблемные билеты
-

📋 Заявки на повреждение СТПК

+

🔧 Ремедиация DSLAM

+
stpk.telecom.kz — Управление проблемными билетами
-
0
-
Всего заявок
+
0
+
Всего билетов
-
0
-
Новые
+
0
+
Проблемные
-
0
-
В работе
+
0
+
В ремедиации
-
0
-
Выполнено
+
0
+
Исправлено
-
-

Новая заявка

-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- -
+
+ +
-
- - - - - - - +
+
+

➕ Новый билет

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ + + + + + + +
+ +
+
+ 0 билетов +
+ +
+ +
+ + + + + + + + + + + + + + + + + +
№DSLAMПортАбонентАдресТип проблемыПриоритетОтветственныйСтатусДатаДействия
+
-
- - - - - - - - - - - - - - - -
№ОбъектТипЗаявительТелефонОписаниеСтатусДатаДействия
+
@@ -150,10 +224,63 @@ let items = []; let currentFilter = "all"; let editId = null; + let lastCount = 0; + let audioContext = null; + + function requestNotificationPermission() { + if ("Notification" in window && Notification.permission === "default") { + Notification.requestPermission(); + } + } + + function playNotificationSound() { + try { + if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const osc = audioContext.createOscillator(); + const gain = audioContext.createGain(); + osc.connect(gain); + gain.connect(audioContext.destination); + osc.frequency.value = 800; + osc.type = "sine"; + gain.gain.setValueAtTime(0.3, audioContext.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4); + osc.start(audioContext.currentTime); + osc.stop(audioContext.currentTime + 0.4); + setTimeout(() => { + const osc2 = audioContext.createOscillator(); + const gain2 = audioContext.createGain(); + osc2.connect(gain2); + gain2.connect(audioContext.destination); + osc2.frequency.value = 1000; + osc2.type = "sine"; + gain2.gain.setValueAtTime(0.3, audioContext.currentTime); + gain2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4); + osc2.start(audioContext.currentTime); + osc2.stop(audioContext.currentTime + 0.4); + }, 150); + } catch (e) { console.log("Audio not supported"); } + } + + function showNotification(title, body) { + playNotificationSound(); + if ("Notification" in window && Notification.permission === "granted") { + new Notification(title, { body: body, icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='45' fill='%230071e3'/%3E%3Ctext x='50' y='65' font-size='50' text-anchor='middle' fill='white'%3E🔧%3C/text%3E%3C/svg%3E" }); + } + const alert = document.createElement("div"); + alert.style.cssText = "position:fixed;top:20px;right:20px;background:#0071e3;color:white;padding:16px 24px;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.2);z-index:1000;"; + alert.innerHTML = "" + title + "
" + body; + document.body.appendChild(alert); + setTimeout(() => alert.remove(), 5000); + } async function load() { const res = await fetch("/api/items"); items = await res.json(); + if (lastCount > 0 && items.length > lastCount) { + const last = items[items.length - 1]; + showNotification("🔔 Новый билет DSLAM", "Порт: " + (last.port||"") + ", Проблема: " + (last.problemType||"")); + } + lastCount = items.length; render(); } @@ -162,85 +289,108 @@ const filtered = items.filter(item => { const matchStatus = currentFilter === "all" || item.status === currentFilter; const matchSearch = !search || - (item.object && item.object.toLowerCase().includes(search)) || - (item.person && item.person.toLowerCase().includes(search)); + (item.dslam && item.dslam.toLowerCase().includes(search)) || + (item.port && item.port.toLowerCase().includes(search)) || + (item.client && item.client.toLowerCase().includes(search)) || + (item.address && item.address.toLowerCase().includes(search)); return matchStatus && matchSearch; }); document.getElementById("table-body").innerHTML = filtered.map(item => { - const date = new Date(item.createdAt).toLocaleDateString("ru-RU"); - return ` - ${item.id} - ${item.object || ""} - ${item.damageType || ""} - ${item.person || ""} - ${item.phone || ""} - ${item.description || ""} + const date = new Date(item.createdAt).toLocaleString("ru-RU"); + const priorityClass = "priority-" + (item.priority === "высокий" || item.priority === "критический" ? "high" : item.priority === "средний" ? "medium" : "low"); + return ` + ${item.id} + ${item.dslam || ""} + ${item.port || ""} + ${item.client || ""} + ${item.address || ""} + ${item.problemType || ""} + ${item.priority || "средний"} + ${item.owner || ""} ${item.status || "новая"} ${date} - - + + + + `; }).join(""); - // Статистика document.getElementById("stat-total").textContent = items.length; - document.getElementById("stat-new").textContent = items.filter(i => i.status === "новая").length; - document.getElementById("stat-work").textContent = items.filter(i => i.status === "в работе").length; - document.getElementById("stat-done").textContent = items.filter(i => i.status === "выполнено").length; + document.getElementById("stat-problem").textContent = items.filter(i => i.status === "новая" || i.status === "в работе").length; + document.getElementById("stat-remedy").textContent = items.filter(i => i.status === "в работе" || i.status === "ожидание").length; + document.getElementById("stat-fixed").textContent = items.filter(i => i.status === "выполнено").length; + document.getElementById("count-display").textContent = filtered.length + " из " + items.length + " билетов"; + } + + function getPriorityColor(p) { + if (p === "критический") return "#c62828"; + if (p === "высокий") return "#e65100"; + if (p === "средний") return "#f9a825"; + return "#2e7d32"; } document.getElementById("form").addEventListener("submit", async (e) => { e.preventDefault(); const form = e.target; const data = { - object: form.object.value, - damageType: form.damageType.value, - person: form.person.value, + dslam: form.dslam.value, + port: form.port.value, + priority: form.priority.value, + client: form.client.value, + address: form.address.value, phone: form.phone.value, + problemType: form.problemType.value, + owner: form.owner.value, description: form.description.value }; if (editId) { - await fetch("/api/items/" + editId, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) - }); + await fetch("/api/items/" + editId, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); editId = null; - form.querySelector("button[type=submit]").textContent = "Подать заявку"; + form.querySelector("button[type=submit]").textContent = "Создать билет"; } else { - await fetch("/api/items", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data) - }); + await fetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) }); } - form.reset(); load(); + requestNotificationPermission(); }); + async function setStatus(id, status) { + const item = items.find(i => i.id === id); + if (!item) return; + await fetch("/api/items/" + id, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: status }) + }); + load(); + } + async function editItem(id) { const item = items.find(i => i.id === id); if (!item) return; - const form = document.getElementById("form"); - form.object.value = item.object || ""; - form.damageType.value = item.damageType || ""; - form.person.value = item.person || ""; + form.dslam.value = item.dslam || ""; + form.port.value = item.port || ""; + form.priority.value = item.priority || "средний"; + form.client.value = item.client || ""; + form.address.value = item.address || ""; form.phone.value = item.phone || ""; + form.problemType.value = item.problemType || ""; + form.owner.value = item.owner || ""; form.description.value = item.description || ""; - editId = id; form.querySelector("button[type=submit]").textContent = "Сохранить изменения"; window.scrollTo({ top: 0, behavior: "smooth" }); } async function deleteItem(id) { - if (!confirm("Вы уверены, что хотите удалить заявку №" + id + "?")) return; + if (!confirm("Удалить билет №" + id + "?")) return; await fetch("/api/items/" + id, { method: "DELETE" }); load(); } @@ -254,18 +404,42 @@ }); }); + document.querySelectorAll(".tab").forEach(tab => { + tab.addEventListener("click", () => { + document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); + tab.classList.add("active"); + if (tab.dataset.view === "log") { + document.getElementById("view-tickets").style.display = "none"; + document.getElementById("view-log").style.display = "block"; + loadLog(); + } else { + document.getElementById("view-tickets").style.display = "block"; + document.getElementById("view-log").style.display = "none"; + } + }); + }); + + async function loadLog() { + const res = await fetch("/api/log"); + const log = await res.json(); + document.getElementById("log-body").innerHTML = log.slice().reverse().map(entry => { + const time = new Date(entry.when).toLocaleString("ru-RU"); + return `${time}${entry.what}${entry.who}`; + }).join(""); + } + document.getElementById("search").addEventListener("input", render); function exportCsv() { - const head = ["№", "Объект", "Тип повреждения", "Заявитель", "Телефон", "Описание", "Статус", "Дата"]; + const head = ["№", "DSLAM", "Порт", "Абонент", "Адрес", "Телефон", "Тип проблемы", "Приоритет", "Ответственный", "Статус", "Описание", "Дата"]; const body = items.map(r => [ - r.id, r.object, r.damageType, r.person, r.phone, r.description, r.status, - new Date(r.createdAt).toLocaleDateString("ru-RU") + r.id, r.dslam, r.port, r.client, r.address, r.phone, r.problemType, r.priority, r.owner, r.status, r.description, + new Date(r.createdAt).toLocaleString("ru-RU") ].map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")); const csv = "\uFEFF" + [head.join(";"), ...body].join("\n"); const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); - a.download = "zayavki_stpk.csv"; + a.download = "bilety_dslam.csv"; a.click(); }