v2: ремедиация DSLAM билеты
This commit is contained in:
parent
62307af33a
commit
a53446e0ad
358
index.html
358
index.html
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user