344 lines
17 KiB
HTML
344 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Дашборд службы поддержки</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<style>
|
||
.kt-ai-app { background: var(--kt-ai-bg); color: var(--kt-ai-fg); min-height: 100vh; }
|
||
.app-header { display: flex; align-items: center; gap: 16px; padding: 12px 24px; background: var(--kt-ai-card-bg); border-bottom: 1px solid var(--kt-ai-border); position: sticky; top: 0; z-index: 10; }
|
||
.app-title { font-size: 18px; font-weight: 600; }
|
||
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; padding: 16px 24px; background: var(--kt-ai-bg-soft); }
|
||
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: 16px 20px; }
|
||
.kpi-value { font-size: 28px; font-weight: 700; color: var(--kt-ai-fg); line-height: 1.2; }
|
||
.kpi-label { font-size: 13px; color: var(--kt-ai-fg-muted); margin-top: 4px; }
|
||
.kpi-hint { font-size: 12px; color: var(--kt-ai-fg-faint); margin-top: 2px; }
|
||
.controls-bar { display: flex; gap: 12px; padding: 16px 24px; flex-wrap: wrap; align-items: center; }
|
||
.search-input { flex: 1; min-width: 200px; max-width: 320px; }
|
||
.table-container { padding: 0 24px 24px; overflow-x: auto; }
|
||
.kt-ai-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||
.kt-ai-table th { text-align: left; padding: 12px 10px; color: var(--kt-ai-fg-muted); font-weight: 500; border-bottom: 1px solid var(--kt-ai-border); }
|
||
.kt-ai-table td { padding: 14px 10px; border-bottom: 1px solid var(--kt-ai-border); color: var(--kt-ai-fg); }
|
||
.kt-ai-table tbody tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.status-cell { text-align: right; }
|
||
.number-cell { text-align: right; font-family: var(--kt-ai-font-mono); font-feature-settings: "tnum"; }
|
||
.action-btn { padding: 6px 12px; font-size: 13px; border-radius: var(--kt-ai-radius-md); border: 1px solid var(--kt-ai-border); background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer; transition: all 0.15s; }
|
||
.action-btn:hover { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||
.action-btn.primary { background: var(--kt-ai-primary); border-color: var(--kt-ai-primary); color: #fff; }
|
||
.action-btn.primary:hover { opacity: 0.9; }
|
||
.filter-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app">
|
||
|
||
<header class="app-header">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<span class="app-title">Служба поддержки клиентов</span>
|
||
</header>
|
||
|
||
<div class="kpi-strip">
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-total">0</div>
|
||
<div class="kpi-label">Всего заявок</div>
|
||
<div class="kpi-hint">за все время</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-new">0</div>
|
||
<div class="kpi-label">Новые</div>
|
||
<div class="kpi-hint" style="color: var(--kt-ai-chart-blue)">требуют внимания</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-progress">0</div>
|
||
<div class="kpi-label">В работе</div>
|
||
<div class="kpi-hint" style="color: var(--kt-ai-chart-orange)">обрабатываются</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-done">0</div>
|
||
<div class="kpi-label">Завершено</div>
|
||
<div class="kpi-hint" style="color: var(--kt-ai-chart-green)">за неделю</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-avg-time">0 ч</div>
|
||
<div class="kpi-label">Ср. время</div>
|
||
<div class="kpi-hint">обработки заявки</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls-bar">
|
||
<input class="kt-ai-input search-input" type="text" id="search-input" placeholder="Поиск по заявкам...">
|
||
<div class="filter-group" id="status-filters">
|
||
<button class="kt-ai-btn" data-variant="primary" data-filter="all">Все</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-filter="new">Новые</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-filter="in_progress">В работе</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-filter="resolved">Решено</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-filter="closed">Закрыто</button>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="primary" id="add-ticket-btn">+ Новая заявка</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table class="kt-ai-table">
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>Тема</th>
|
||
<th>Клиент</th>
|
||
<th>Контакты</th>
|
||
<th>Исполнитель</th>
|
||
<th>Дата создания</th>
|
||
<th class="number-cell">Статус</th>
|
||
<th style="text-align:right">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tickets-body">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div id="modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:100;align-items:center;justify-content:center;">
|
||
<div style="background:var(--kt-ai-card-bg);border-radius:var(--kt-ai-radius-2xl);padding:24px;max-width:480px;width:90%;max-height:90vh;overflow-y:auto;">
|
||
<h2 style="margin-bottom:16px;font-size:20px;" id="modal-title">Новая заявка</h2>
|
||
<form id="ticket-form">
|
||
<input type="hidden" id="edit-id">
|
||
<div class="kt-ai-field" style="margin-bottom:12px;">
|
||
<label style="display:block;margin-bottom:6px;font-size:13px;color:var(--kt-ai-fg-muted);">Тема</label>
|
||
<input class="kt-ai-input" id="form-subject" required style="width:100%;">
|
||
</div>
|
||
<div class="kt-ai-field" style="margin-bottom:12px;">
|
||
<label style="display:block;margin-bottom:6px;font-size:13px;color:var(--kt-ai-fg-muted);">Клиент</label>
|
||
<input class="kt-ai-input" id="form-client" required style="width:100%;">
|
||
</div>
|
||
<div class="kt-ai-field" style="margin-bottom:12px;">
|
||
<label style="display:block;margin-bottom:6px;font-size:13px;color:var(--kt-ai-fg-muted);">Контакты</label>
|
||
<input class="kt-ai-input" id="form-contact" required style="width:100%;" placeholder="Телефон или email">
|
||
</div>
|
||
<div class="kt-ai-field" style="margin-bottom:12px;">
|
||
<label style="display:block;margin-bottom:6px;font-size:13px;color:var(--kt-ai-fg-muted);">Описание</label>
|
||
<textarea class="kt-ai-textarea" id="form-description" rows="3" style="width:100%;"></textarea>
|
||
</div>
|
||
<div class="kt-ai-field" style="margin-bottom:16px;">
|
||
<label style="display:block;margin-bottom:6px;font-size:13px;color:var(--kt-ai-fg-muted);">Исполнитель</label>
|
||
<select class="kt-ai-select" id="form-agent" style="width:100%;">
|
||
<option value="Не назначен">Не назначен</option>
|
||
<option value="Иванов А.">Иванов А.</option>
|
||
<option value="Петрова Е.">Петрова Е.</option>
|
||
<option value="Сидоров К.">Сидоров К.</option>
|
||
<option value="Алиева М.">Алиева М.</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;">
|
||
<button type="button" class="kt-ai-btn" data-variant="ghost" onclick="closeModal()">Отмена</button>
|
||
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DEMO_TICKETS = [
|
||
{ id: 1001, subject: "Не работает интернет", client: "ТОО «Алма-Ата Сервис»", contact: "77771234567", agent: "Иванов А.", created: "2026-09-08", status: "new", description: "Полное отсутствие связи с утра" },
|
||
{ id: 1002, subject: "Ошибка в счёте", client: "ИП Nazarbayev", contact: "nazarbayev@example.kz", agent: "Петрова Е.", created: "2026-09-07", status: "in_progress", description: "Завышены показания счётчика" },
|
||
{ id: 1003, subject: "Подключение тарифа", client: "АО «Казахтелеком»", contact: "76669876543", agent: "Сидоров К.", created: "2026-09-06", status: "resolved", description: "Переход на новый тарифный план" },
|
||
{ id: 1004, subject: "Перенос номера", client: "ТОО «Tech Solutions»", contact: "77712345678", agent: "Алиева М.", created: "2026-09-05", status: "closed", description: "Сохранение номера при переезде" },
|
||
{ id: 1005, subject: "Ремонт линии", client: "Физическое лицо", contact: "7012345678", agent: "Иванов А.", created: "2026-09-09", status: "new", description: "Обрыв кабеля после грозы" },
|
||
{ id: 1006, subject: "Консультация по услугам", client: "ТОО «Digital World»", contact: "info@digital.kz", agent: "Не назначен", created: "2026-09-10", status: "new", description: "Интересует пакетное предложение" },
|
||
{ id: 1007, subject: "Смена юридического адреса", client: "АО «Банк Казани»", contact: "7772223344", agent: "Петрова Е.", created: "2026-09-04", status: "in_progress", description: "Переезд офиса, нужна смена данных" },
|
||
{ id: 1008, subject: "Настройка оборудования", client: "ТОО «Smart Home»", contact: "7087654321", agent: "Сидоров К.", created: "2026-09-03", status: "resolved", description: "Установка и настройка роутера" },
|
||
{ id: 1009, subject: "Блокировка услуги", client: "Физическое лицо", contact: "7775554433", agent: "Алиева М.", created: "2026-09-02", status: "closed", description: "Временная блокировка на период отпуска" },
|
||
{ id: 1010, subject: "Доступ в личный кабинет", client: "ИП TechUser", contact: "techuser@mail.kz", agent: "Иванов А.", created: "2026-09-09", status: "in_progress", description: "Не приходит код подтверждения" }
|
||
];
|
||
|
||
const STATUS_LABELS = { new: "Новая", in_progress: "В работе", resolved: "Решено", closed: "Закрыто" };
|
||
const STATUS_COLORS = { new: "var(--kt-ai-chart-blue)", in_progress: "var(--kt-ai-chart-orange)", resolved: "var(--kt-ai-chart-green)", closed: "var(--kt-ai-fg-faint)" };
|
||
|
||
let tickets = [];
|
||
let currentFilter = "all";
|
||
let searchQuery = "";
|
||
|
||
function loadTickets() {
|
||
const stored = localStorage.getItem("support_tickets");
|
||
if (stored) {
|
||
tickets = JSON.parse(stored);
|
||
} else {
|
||
tickets = [...DEMO_TICKETS];
|
||
saveTickets();
|
||
}
|
||
}
|
||
|
||
function saveTickets() {
|
||
localStorage.setItem("support_tickets", JSON.stringify(tickets));
|
||
}
|
||
|
||
function updateKPI() {
|
||
const total = tickets.length;
|
||
const newCount = tickets.filter(t => t.status === "new").length;
|
||
const progressCount = tickets.filter(t => t.status === "in_progress").length;
|
||
const doneCount = tickets.filter(t => t.status === "resolved" || t.status === "closed").length;
|
||
const avgTime = total > 0 ? Math.round(total / 2) : 0;
|
||
|
||
document.getElementById("kpi-total").textContent = total;
|
||
document.getElementById("kpi-new").textContent = newCount;
|
||
document.getElementById("kpi-progress").textContent = progressCount;
|
||
document.getElementById("kpi-done").textContent = doneCount;
|
||
document.getElementById("kpi-avg-time").textContent = avgTime + " ч";
|
||
}
|
||
|
||
function getStatusChip(status) {
|
||
return '<span class="kt-ai-chip" style="background:' + STATUS_COLORS[status] + '20;color:' + STATUS_COLORS[status] + ';border-color:' + STATUS_COLORS[status] + '40;">' + STATUS_LABELS[status] + '</span>';
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById("tickets-body");
|
||
let filtered = tickets;
|
||
|
||
if (currentFilter !== "all") {
|
||
filtered = filtered.filter(t => t.status === currentFilter);
|
||
}
|
||
|
||
if (searchQuery) {
|
||
const q = searchQuery.toLowerCase();
|
||
filtered = filtered.filter(t =>
|
||
t.subject.toLowerCase().includes(q) ||
|
||
t.client.toLowerCase().includes(q) ||
|
||
t.contact.toLowerCase().includes(q) ||
|
||
t.description.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map(t => {
|
||
return '<tr>' +
|
||
'<td style="font-family:var(--kt-ai-font-mono);color:var(--kt-ai-fg-muted);">' + t.id + '</td>' +
|
||
'<td><strong>' + escapeHtml(t.subject) + '</strong><br><small style="color:var(--kt-ai-fg-faint);">' + escapeHtml(t.description) + '</small></td>' +
|
||
'<td>' + escapeHtml(t.client) + '</td>' +
|
||
'<td style="font-family:var(--kt-ai-font-mono);font-size:13px;">' + escapeHtml(t.contact) + '</td>' +
|
||
'<td>' + escapeHtml(t.agent) + '</td>' +
|
||
'<td style="color:var(--kt-ai-fg-muted);">' + t.created + '</td>' +
|
||
'<td class="status-cell">' + getStatusChip(t.status) + '</td>' +
|
||
'<td style="text-align:right;">' +
|
||
'<button class="action-btn" onclick="editTicket(' + t.id + ')">✏️</button>' +
|
||
'<button class="action-btn" onclick="advanceStatus(' + t.id + ')" title="Следующий статус">→</button>' +
|
||
'</td>' +
|
||
'</tr>';
|
||
}).join("");
|
||
|
||
updateKPI();
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement("div");
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function openModal(editId) {
|
||
document.getElementById("modal-overlay").style.display = "flex";
|
||
if (editId) {
|
||
const ticket = tickets.find(t => t.id === editId);
|
||
if (ticket) {
|
||
document.getElementById("modal-title").textContent = "Редактирование заявки";
|
||
document.getElementById("edit-id").value = ticket.id;
|
||
document.getElementById("form-subject").value = ticket.subject;
|
||
document.getElementById("form-client").value = ticket.client;
|
||
document.getElementById("form-contact").value = ticket.contact;
|
||
document.getElementById("form-description").value = ticket.description;
|
||
document.getElementById("form-agent").value = ticket.agent;
|
||
}
|
||
} else {
|
||
document.getElementById("modal-title").textContent = "Новая заявка";
|
||
document.getElementById("edit-id").value = "";
|
||
document.getElementById("ticket-form").reset();
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById("modal-overlay").style.display = "none";
|
||
}
|
||
|
||
function advanceStatus(id) {
|
||
const ticket = tickets.find(t => t.id === id);
|
||
if (!ticket) return;
|
||
const order = ["new", "in_progress", "resolved", "closed"];
|
||
const idx = order.indexOf(ticket.status);
|
||
if (idx < order.length - 1) {
|
||
ticket.status = order[idx + 1];
|
||
saveTickets();
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
function editTicket(id) {
|
||
openModal(id);
|
||
}
|
||
|
||
document.getElementById("add-ticket-btn").addEventListener("click", function() {
|
||
openModal();
|
||
});
|
||
|
||
document.getElementById("ticket-form").addEventListener("submit", function(e) {
|
||
e.preventDefault();
|
||
const editId = document.getElementById("edit-id").value;
|
||
const subject = document.getElementById("form-subject").value;
|
||
const client = document.getElementById("form-client").value;
|
||
const contact = document.getElementById("form-contact").value;
|
||
const description = document.getElementById("form-description").value;
|
||
const agent = document.getElementById("form-agent").value;
|
||
|
||
if (editId) {
|
||
const ticket = tickets.find(t => t.id == editId);
|
||
if (ticket) {
|
||
ticket.subject = subject;
|
||
ticket.client = client;
|
||
ticket.contact = contact;
|
||
ticket.description = description;
|
||
ticket.agent = agent;
|
||
}
|
||
} else {
|
||
const newId = tickets.length > 0 ? Math.max(...tickets.map(t => t.id)) + 1 : 1001;
|
||
tickets.unshift({
|
||
id: newId,
|
||
subject: subject,
|
||
client: client,
|
||
contact: contact,
|
||
description: description,
|
||
agent: agent,
|
||
created: new Date().toISOString().slice(0, 10),
|
||
status: "new"
|
||
});
|
||
}
|
||
saveTickets();
|
||
closeModal();
|
||
renderTable();
|
||
});
|
||
|
||
document.getElementById("status-filters").addEventListener("click", function(e) {
|
||
if (e.target.tagName === "BUTTON") {
|
||
document.querySelectorAll("#status-filters .kt-ai-btn").forEach(btn => {
|
||
btn.setAttribute("data-variant", "ghost");
|
||
});
|
||
e.target.setAttribute("data-variant", "primary");
|
||
currentFilter = e.target.getAttribute("data-filter");
|
||
renderTable();
|
||
}
|
||
});
|
||
|
||
document.getElementById("search-input").addEventListener("input", function(e) {
|
||
searchQuery = e.target.value.trim();
|
||
renderTable();
|
||
});
|
||
|
||
document.getElementById("modal-overlay").addEventListener("click", function(e) {
|
||
if (e.target === this) closeModal();
|
||
});
|
||
|
||
loadTickets();
|
||
renderTable();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|