-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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();
}