deshbord-sluzhbe-podderzhki/index.html

344 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>