v2: ремедиация DSLAM билеты

This commit is contained in:
Altynbek 2026-08-14 06:46:17 +00:00
parent 62307af33a
commit a53446e0ad

View File

@ -3,18 +3,27 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Заявки на повреждение СТПК</title> <title>Ремедиация DSLAM — Проблемные билеты</title>
<style> <style>
* { margin: 0; box-sizing: border-box; } * { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; padding: 20px; } body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; } .container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1d1d1f; } h1 { font-size: 28px; margin-bottom: 8px; color: #1d1d1f; }
.subtitle { color: #86868b; margin-bottom: 24px; font-size: 14px; }
.stats { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; } .stats { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.stat { background: white; padding: 16px 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .stat { background: white; padding: 16px 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); flex: 1; min-width: 140px; }
.stat-value { font-size: 32px; font-weight: 700; color: #0071e3; } .stat-value { font-size: 32px; font-weight: 700; }
.stat-total { color: #0071e3; }
.stat-problem { color: #e30000; }
.stat-remedy { color: #f57c00; }
.stat-fixed { color: #388e3c; }
.stat-label { font-size: 14px; color: #86868b; margin-top: 4px; } .stat-label { font-size: 14px; color: #86868b; margin-top: 4px; }
.tabs { display: flex; gap: 8px; margin-bottom: 24px; border-bottom: 2px solid #e5e5e5; padding-bottom: 8px; }
.tab { padding: 8px 20px; border: none; background: none; cursor: pointer; font-size: 14px; color: #86868b; border-radius: 8px 8px 0 0; }
.tab.active { background: #0071e3; color: white; }
.filters { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; align-items: center; } .filters { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; align-items: center; }
.filter-btn { padding: 8px 16px; border: 1px solid #d2d2d7; background: white; border-radius: 20px; cursor: pointer; font-size: 14px; } .filter-btn { padding: 8px 16px; border: 1px solid #d2d2d7; background: white; border-radius: 20px; cursor: pointer; font-size: 14px; }
.filter-btn.active { background: #0071e3; color: white; border-color: #0071e3; } .filter-btn.active { background: #0071e3; color: white; border-color: #0071e3; }
@ -22,84 +31,106 @@
.form-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; } .form-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 24px; }
.form-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; } .form-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
.form-group { flex: 1; min-width: 200px; } .form-group { flex: 1; min-width: 150px; }
label { display: block; font-size: 14px; color: #86868b; margin-bottom: 6px; } .form-group.full { flex-basis: 100%; }
label { display: block; font-size: 13px; color: #86868b; margin-bottom: 6px; font-weight: 500; }
input, select, textarea { width: 100%; padding: 10px 14px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; } input, select, textarea { width: 100%; padding: 10px 14px; border: 1px solid #d2d2d7; border-radius: 8px; font-size: 14px; }
textarea { resize: vertical; min-height: 80px; } textarea { resize: vertical; min-height: 70px; }
.btn { padding: 10px 24px; background: #0071e3; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; } .btn { padding: 10px 24px; background: #0071e3; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn:hover { background: #005bb5; } .btn:hover { background: #005bb5; }
.btn-secondary { background: #86868b; } .btn-secondary { background: #86868b; }
.btn-danger { background: #e30000; } .btn-danger { background: #e30000; }
.btn-success { background: #388e3c; }
.btn-warning { background: #f57c00; }
.table-card { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; } .table-card { background: white; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow-x: auto; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; min-width: 900px; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e5e5; } th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #e5e5e5; font-size: 13px; }
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #86868b; } th { background: #f5f5f7; font-weight: 600; color: #86868b; white-space: nowrap; }
tr:hover { background: #f9f9f9; } tr:hover { background: #f9f9f9; }
.status { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; } .status { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; white-space: nowrap; }
.status-новая { background: #e3f2fd; color: #1976d2; } .status-новая { background: #e3f2fd; color: #1976d2; }
.status-в-работе { background: #fff3e0; color: #f57c00; } .status-в-работе { background: #fff3e0; color: #f57c00; }
.status-ожидание { background: #fce4ec; color: #c2185b; }
.status-выполнено { background: #e8f5e9; color: #388e3c; } .status-выполнено { background: #e8f5e9; color: #388e3c; }
.status-закрыто { background: #f5f5f5; color: #86868b; } .status-закрыто { background: #f5f5f5; color: #86868b; }
.actions { display: flex; gap: 8px; } .actions { display: flex; gap: 6px; white-space: nowrap; }
.btn-sm { padding: 6px 12px; font-size: 12px; } .btn-sm { padding: 5px 10px; font-size: 11px; }
.export-btn { margin-bottom: 16px; } .priority-high { background: #ffebee; }
.priority-medium { background: #fff8e1; }
.priority-low { background: #e8f5e9; }
@media (max-width: 768px) { .export-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; background: #e5e5e5; color: #333; }
@media (max-width: 1024px) {
.form-row { flex-direction: column; } .form-row { flex-direction: column; }
.stats { flex-direction: column; } .stats { flex-direction: column; }
table { font-size: 13px; }
th, td { padding: 8px; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>📋 Заявки на повреждение СТПК</h1> <h1>🔧 Ремедиация DSLAM</h1>
<div class="subtitle">stpk.telecom.kz — Управление проблемными билетами</div>
<div class="stats"> <div class="stats">
<div class="stat"> <div class="stat">
<div class="stat-value" id="stat-total">0</div> <div class="stat-value stat-total" id="stat-total">0</div>
<div class="stat-label">Всего заявок</div> <div class="stat-label">Всего билетов</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-value" id="stat-new">0</div> <div class="stat-value stat-problem" id="stat-problem">0</div>
<div class="stat-label">Новые</div> <div class="stat-label">Проблемные</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-value" id="stat-work">0</div> <div class="stat-value stat-remedy" id="stat-remedy">0</div>
<div class="stat-label">В работе</div> <div class="stat-label">В ремедиации</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-value" id="stat-done">0</div> <div class="stat-value stat-fixed" id="stat-fixed">0</div>
<div class="stat-label">Выполнено</div> <div class="stat-label">Исправлено</div>
</div> </div>
</div> </div>
<div class="tabs">
<button class="tab active" data-view="tickets">Билеты</button>
<button class="tab" data-view="log">Лог действий</button>
</div>
<div id="view-tickets">
<div class="form-card"> <div class="form-card">
<h2 style="margin-bottom: 16px; font-size: 18px;">Новая заявка</h2> <h2 style="margin-bottom: 16px; font-size: 18px;">➕ Новый билет</h2>
<form id="form"> <form id="form">
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Объект</label> <label>DSLAM</label>
<input type="text" name="object" placeholder="Например: СТПК-1" required> <input type="text" name="dslam" placeholder="Например: DSLAM-01" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Тип повреждения</label> <label>Порт</label>
<select name="damageType" required> <input type="text" name="port" placeholder="Например: 1/0/5" required>
<option value="">Выберите тип</option> </div>
<option value="механическое">Механическое</option> <div class="form-group">
<option value="электрическое">Электрическое</option> <label>Приоритет</label>
<option value="коррозия">Коррозия</option> <select name="priority" required>
<option value="другое">Другое</option> <option value="низкий">Низкий</option>
<option value="средний" selected>Средний</option>
<option value="высокий">Высокий</option>
<option value="критический">Критический</option>
</select> </select>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Заявитель</label> <label>Абонент</label>
<input type="text" name="person" placeholder="ФИО" required> <input type="text" name="client" placeholder="ФИО или название организации" required>
</div>
<div class="form-group">
<label>Адрес</label>
<input type="text" name="address" placeholder="Улица, дом, квартира" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Телефон</label> <label>Телефон</label>
@ -108,22 +139,47 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Описание повреждения</label> <label>Тип проблемы</label>
<textarea name="description" placeholder="Опишите характер повреждения..." required></textarea> <select name="problemType" required>
<option value="">Выберите тип</option>
<option value="нет связи">Нет связи</option>
<option value="низкая скорость">Низкая скорость</option>
<option value="обрыв линии">Обрыв линии</option>
<option value="ошибка порта">Ошибка порта</option>
<option value="шумы в линии">Шумы в линии</option>
<option value="другое">Другое</option>
</select>
</div>
<div class="form-group">
<label>Ответственный</label>
<input type="text" name="owner" placeholder="ФИО сотрудника" required>
</div> </div>
</div> </div>
<button type="submit" class="btn">Подать заявку</button> <div class="form-row">
<div class="form-group full">
<label>Описание проблемы</label>
<textarea name="description" placeholder="Подробное описание проблемы, результаты диагностики..." required></textarea>
</div>
</div>
<button type="submit" class="btn">Создать билет</button>
</form> </form>
</div> </div>
<div class="filters"> <div class="filters">
<button class="filter-btn active" data-status="all">Все</button> <button class="filter-btn active" data-status="all">Все</button>
<button class="filter-btn" data-status="новая">Новые</button> <button class="filter-btn" data-status="новая">Новые</button>
<button class="filter-btn" data-status="в-работе">В работе</button> <button class="filter-btn" data-status="в работе">В работе</button>
<button class="filter-btn" data-status="ожидание">Ожидание</button>
<button class="filter-btn" data-status="выполнено">Выполнено</button> <button class="filter-btn" data-status="выполнено">Выполнено</button>
<button class="filter-btn" data-status="закрыто">Закрыто</button> <button class="filter-btn" data-status="закрыто">Закрыто</button>
<input type="text" class="search" id="search" placeholder="Поиск по объекту или заявителю..."> <input type="text" class="search" id="search" placeholder="Поиск по DSLAM, порту, абоненту...">
<button class="btn export-btn" onclick="exportCsv()">📥 Выгрузить в Excel</button> </div>
<div class="export-row">
<div style="font-size: 14px; color: #86868b;">
<span id="count-display">0 билетов</span>
</div>
<button class="btn" onclick="exportCsv()">📥 Выгрузить в Excel</button>
</div> </div>
<div class="table-card"> <div class="table-card">
@ -131,11 +187,13 @@
<thead> <thead>
<tr> <tr>
<th>№</th> <th>№</th>
<th>Объект</th> <th>DSLAM</th>
<th>Тип</th> <th>Порт</th>
<th>Заявитель</th> <th>Абонент</th>
<th>Телефон</th> <th>Адрес</th>
<th>Описание</th> <th>Тип проблемы</th>
<th>Приоритет</th>
<th>Ответственный</th>
<th>Статус</th> <th>Статус</th>
<th>Дата</th> <th>Дата</th>
<th>Действия</th> <th>Действия</th>
@ -146,14 +204,83 @@
</div> </div>
</div> </div>
<div id="view-log" style="display: none;">
<div class="table-card">
<table>
<thead>
<tr>
<th>Время</th>
<th>Событие</th>
<th>Кто</th>
</tr>
</thead>
<tbody id="log-body"></tbody>
</table>
</div>
</div>
</div>
<script> <script>
let items = []; let items = [];
let currentFilter = "all"; let currentFilter = "all";
let editId = null; 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 = "<strong>" + title + "</strong><br>" + body;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}
async function load() { async function load() {
const res = await fetch("/api/items"); const res = await fetch("/api/items");
items = await res.json(); 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(); render();
} }
@ -162,85 +289,108 @@
const filtered = items.filter(item => { const filtered = items.filter(item => {
const matchStatus = currentFilter === "all" || item.status === currentFilter; const matchStatus = currentFilter === "all" || item.status === currentFilter;
const matchSearch = !search || const matchSearch = !search ||
(item.object && item.object.toLowerCase().includes(search)) || (item.dslam && item.dslam.toLowerCase().includes(search)) ||
(item.person && item.person.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; return matchStatus && matchSearch;
}); });
document.getElementById("table-body").innerHTML = filtered.map(item => { document.getElementById("table-body").innerHTML = filtered.map(item => {
const date = new Date(item.createdAt).toLocaleDateString("ru-RU"); const date = new Date(item.createdAt).toLocaleString("ru-RU");
return `<tr> const priorityClass = "priority-" + (item.priority === "высокий" || item.priority === "критический" ? "high" : item.priority === "средний" ? "medium" : "low");
<td>${item.id}</td> return `<tr class="${priorityClass}">
<td>${item.object || ""}</td> <td><strong>${item.id}</strong></td>
<td>${item.damageType || ""}</td> <td>${item.dslam || ""}</td>
<td>${item.person || ""}</td> <td><span class="badge">${item.port || ""}</span></td>
<td>${item.phone || ""}</td> <td>${item.client || ""}</td>
<td>${item.description || ""}</td> <td>${item.address || ""}</td>
<td>${item.problemType || ""}</td>
<td><span class="badge" style="background:${getPriorityColor(item.priority)};color:white">${item.priority || "средний"}</span></td>
<td>${item.owner || ""}</td>
<td><span class="status status-${(item.status||'новая').replace(' ','-')}">${item.status || "новая"}</span></td> <td><span class="status status-${(item.status||'новая').replace(' ','-')}">${item.status || "новая"}</span></td>
<td>${date}</td> <td>${date}</td>
<td class="actions"> <td class="actions">
<button class="btn btn-sm btn-secondary" onclick="editItem(${item.id})">Изменить</button> <button class="btn btn-sm btn-warning" onclick="setStatus(${item.id},'в работе')" title="В работу">▶</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">Удалить</button> <button class="btn btn-sm btn-success" onclick="setStatus(${item.id},'выполнено')" title="Выполнено">✓</button>
<button class="btn btn-sm btn-secondary" onclick="editItem(${item.id})">✎</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">✕</button>
</td> </td>
</tr>`; </tr>`;
}).join(""); }).join("");
// Статистика
document.getElementById("stat-total").textContent = items.length; document.getElementById("stat-total").textContent = items.length;
document.getElementById("stat-new").textContent = items.filter(i => i.status === "новая").length; document.getElementById("stat-problem").textContent = items.filter(i => i.status === "новая" || i.status === "в работе").length;
document.getElementById("stat-work").textContent = items.filter(i => i.status === "в работе").length; document.getElementById("stat-remedy").textContent = items.filter(i => i.status === "в работе" || i.status === "ожидание").length;
document.getElementById("stat-done").textContent = items.filter(i => 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) => { document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
const form = e.target; const form = e.target;
const data = { const data = {
object: form.object.value, dslam: form.dslam.value,
damageType: form.damageType.value, port: form.port.value,
person: form.person.value, priority: form.priority.value,
client: form.client.value,
address: form.address.value,
phone: form.phone.value, phone: form.phone.value,
problemType: form.problemType.value,
owner: form.owner.value,
description: form.description.value description: form.description.value
}; };
if (editId) { if (editId) {
await fetch("/api/items/" + editId, { await fetch("/api/items/" + editId, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
editId = null; editId = null;
form.querySelector("button[type=submit]").textContent = "Подать заявку"; form.querySelector("button[type=submit]").textContent = "Создать билет";
} else { } else {
await fetch("/api/items", { await fetch("/api/items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
} }
form.reset(); form.reset();
load(); 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) { async function editItem(id) {
const item = items.find(i => i.id === id); const item = items.find(i => i.id === id);
if (!item) return; if (!item) return;
const form = document.getElementById("form"); const form = document.getElementById("form");
form.object.value = item.object || ""; form.dslam.value = item.dslam || "";
form.damageType.value = item.damageType || ""; form.port.value = item.port || "";
form.person.value = item.person || ""; form.priority.value = item.priority || "средний";
form.client.value = item.client || "";
form.address.value = item.address || "";
form.phone.value = item.phone || ""; form.phone.value = item.phone || "";
form.problemType.value = item.problemType || "";
form.owner.value = item.owner || "";
form.description.value = item.description || ""; form.description.value = item.description || "";
editId = id; editId = id;
form.querySelector("button[type=submit]").textContent = "Сохранить изменения"; form.querySelector("button[type=submit]").textContent = "Сохранить изменения";
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
async function deleteItem(id) { async function deleteItem(id) {
if (!confirm("Вы уверены, что хотите удалить заявку №" + id + "?")) return; if (!confirm("Удалить билет №" + id + "?")) return;
await fetch("/api/items/" + id, { method: "DELETE" }); await fetch("/api/items/" + id, { method: "DELETE" });
load(); 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 `<tr><td>${time}</td><td>${entry.what}</td><td>${entry.who}</td></tr>`;
}).join("");
}
document.getElementById("search").addEventListener("input", render); document.getElementById("search").addEventListener("input", render);
function exportCsv() { function exportCsv() {
const head = ["№", "Объект", "Тип повреждения", "Заявитель", "Телефон", "Описание", "Статус", "Дата"]; const head = ["№", "DSLAM", "Порт", "Абонент", "Адрес", "Телефон", "Тип проблемы", "Приоритет", "Ответственный", "Статус", "Описание", "Дата"];
const body = items.map(r => [ const body = items.map(r => [
r.id, r.object, r.damageType, r.person, r.phone, r.description, r.status, 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).toLocaleDateString("ru-RU") new Date(r.createdAt).toLocaleString("ru-RU")
].map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";")); ].map(v => '"' + String(v ?? "").replace(/"/g, '""') + '"').join(";"));
const csv = "\uFEFF" + [head.join(";"), ...body].join("\n"); const csv = "\uFEFF" + [head.join(";"), ...body].join("\n");
const a = document.createElement("a"); const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
a.download = "zayavki_stpk.csv"; a.download = "bilety_dslam.csv";
a.click(); a.click();
} }