497 lines
28 KiB
HTML
497 lines
28 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">
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎧</text></svg>">
|
||
<style>
|
||
a.kt-ai-btn { text-decoration: none; }
|
||
body.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; }
|
||
.app-content { flex: 1; padding-bottom: var(--kt-ai-space-8); }
|
||
.kpi-strip { display: flex; gap: var(--kt-ai-space-4); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
|
||
.kpi { flex: 1; min-width: 160px; background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||
.kpi .value { font-size: 28px; font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); line-height: 1.2; }
|
||
.kpi .label { font-size: 13px; color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
|
||
.kpi .hint { font-size: 12px; color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
|
||
.chart-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-6); }
|
||
.chart { display: flex; align-items: flex-end; gap: var(--kt-ai-space-3); height: 200px; padding-top: var(--kt-ai-space-4); }
|
||
.chart-bar { flex: 1; background: var(--kt-ai-chart-blue); border-radius: var(--kt-ai-radius-md); min-width: 24px; position: relative; transition: opacity 0.2s; cursor: pointer; }
|
||
.chart-bar:hover { opacity: 0.8; }
|
||
.chart-bar[data-kt-tip]::after { content: attr(data-kt-tip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: var(--kt-ai-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-md); padding: 4px 8px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; margin-bottom: 6px; }
|
||
.chart-bar:hover::after { opacity: 1; }
|
||
.chart-labels { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-3); }
|
||
.chart-label { flex: 1; min-width: 24px; font-size: 11px; color: var(--kt-ai-fg-muted); text-align: center; }
|
||
.filters { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-5); }
|
||
.filter-chip { padding: 6px 12px; border-radius: var(--kt-ai-radius-full); font-size: 13px; cursor: pointer; border: 1px solid var(--kt-ai-border); background: var(--kt-ai-card-bg); color: var(--kt-ai-fg-muted); transition: all 0.2s; }
|
||
.filter-chip:hover { border-color: var(--kt-ai-primary); }
|
||
.filter-chip.active { background: var(--kt-ai-primary); color: var(--kt-ai-fg); border-color: var(--kt-ai-primary); }
|
||
.table-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); overflow: hidden; }
|
||
.table { width: 100%; border-collapse: collapse; }
|
||
.table th { text-align: left; font-size: 12px; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); background: var(--kt-ai-bg-soft); }
|
||
.table td { padding: var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); font-size: 14px; color: var(--kt-ai-fg); }
|
||
.table tbody tr { cursor: pointer; transition: background 0.2s; }
|
||
.table tbody tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.table tbody tr:last-child td { border-bottom: none; }
|
||
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.status { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--kt-ai-radius-full); font-size: 12px; font-weight: var(--kt-ai-weight-medium); }
|
||
.status.new { background: var(--kt-ai-chart-blue); color: var(--kt-ai-fg); }
|
||
.status.progress { background: var(--kt-ai-chart-orange); color: var(--kt-ai-fg); }
|
||
.status.done { background: var(--kt-ai-chart-green); color: var(--kt-ai-fg); }
|
||
.status.overdue { background: var(--kt-ai-chart-red); color: var(--kt-ai-fg); }
|
||
.priority { display: inline-flex; gap: 3px; }
|
||
.priority-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--kt-ai-fg-faint); }
|
||
.priority-dot.active.low { background: var(--kt-ai-chart-green); }
|
||
.priority-dot.active.medium { background: var(--kt-ai-chart-orange); }
|
||
.priority-dot.active.high { background: var(--kt-ai-chart-red); }
|
||
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); opacity: 0; pointer-events: none; transition: opacity 0.3s; z-index: 100; }
|
||
.drawer-overlay.open { opacity: 1; pointer-events: auto; }
|
||
.drawer { position: fixed; right: 0; top: 0; bottom: 0; width: 480px; max-width: 100vw; background: var(--kt-ai-bg); transform: translateX(100%); transition: transform 0.3s; z-index: 101; display: flex; flex-direction: column; }
|
||
.drawer.open { transform: translateX(0); }
|
||
.drawer-header { padding: var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); display: flex; align-items: center; justify-content: space-between; }
|
||
.drawer-header h2 { font-size: 18px; font-weight: var(--kt-ai-weight-semibold); margin: 0; }
|
||
.drawer-close { background: none; border: none; font-size: 24px; color: var(--kt-ai-fg-muted); cursor: pointer; padding: 4px; }
|
||
.drawer-body { flex: 1; overflow-y: auto; padding: var(--kt-ai-space-5); }
|
||
.detail-row { display: flex; justify-content: space-between; padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-border); }
|
||
.detail-row:last-child { border-bottom: none; }
|
||
.detail-label { color: var(--kt-ai-fg-muted); font-size: 13px; }
|
||
.detail-value { font-size: 14px; color: var(--kt-ai-fg); text-align: right; }
|
||
.empty { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
|
||
.empty-icon { font-size: 48px; margin-bottom: var(--kt-ai-space-4); opacity: 0.5; }
|
||
.btn-icon { background: none; border: none; font-size: 20px; cursor: pointer; color: var(--kt-ai-fg-muted); padding: 8px; border-radius: var(--kt-ai-radius-md); }
|
||
.btn-icon:hover { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||
.header-title { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 200; opacity: 0; pointer-events: none; transition: opacity 0.3s; }
|
||
.modal-overlay.open { opacity: 1; pointer-events: auto; }
|
||
.modal { background: var(--kt-ai-bg); border-radius: var(--kt-ai-radius-2xl); width: 90%; max-width: 520px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-header { padding: var(--kt-ai-space-5); border-bottom: 1px solid var(--kt-ai-border); display: flex; justify-content: space-between; align-items: center; }
|
||
.modal-body { padding: var(--kt-ai-space-5); }
|
||
.modal-footer { padding: var(--kt-ai-space-4) var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; }
|
||
@media (max-width: 768px) {
|
||
.kpi-strip { flex-direction: column; }
|
||
.filters { flex-direction: column; align-items: stretch; }
|
||
.table th, .table td { padding: var(--kt-ai-space-2) var(--kt-ai-space-3); font-size: 13px; }
|
||
.drawer { width: 100%; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app" data-kt-data-mode="demo">
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<div class="header-title">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<strong>Служба поддержки</strong>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="openNewTicketModal()">+ Заявка</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="kt-ai-wrap app-content" style="padding-top: var(--kt-ai-space-6)">
|
||
|
||
<div class="kpi-strip">
|
||
<div class="kpi">
|
||
<div class="value" id="kpi-total">0</div>
|
||
<div class="label">Всего заявок</div>
|
||
<div class="hint">за выбранный период</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="value" id="kpi-progress">0</div>
|
||
<div class="label">В работе</div>
|
||
<div class="hint">требуют внимания</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="value" id="kpi-done">0</div>
|
||
<div class="label">Решено</div>
|
||
<div class="hint">успешно закрыто</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="value" id="kpi-overdue" style="color: var(--kt-ai-chart-red)">0</div>
|
||
<div class="label">Просрочено</div>
|
||
<div class="hint">требуют срочного решения</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chart-wrap">
|
||
<h3 style="margin: 0 0 var(--kt-ai-space-4) 0; font-size: 16px;">Заявки по дням</h3>
|
||
<div class="chart" id="chart"></div>
|
||
<div class="chart-labels" id="chart-labels"></div>
|
||
</div>
|
||
|
||
<div class="filters">
|
||
<span style="font-size: 13px; color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium)">Фильтры:</span>
|
||
<button class="filter-chip active" data-filter="status" data-value="all">Все статусы</button>
|
||
<button class="filter-chip" data-filter="status" data-value="new">Новые</button>
|
||
<button class="filter-chip" data-filter="status" data-value="progress">В работе</button>
|
||
<button class="filter-chip" data-filter="status" data-value="done">Решено</button>
|
||
<button class="filter-chip" data-filter="status" data-value="overdue">Просрочено</button>
|
||
<span class="kt-ai-spacer" style="flex: 1"></span>
|
||
<select class="kt-ai-select" id="priority-filter" style="min-width: 140px">
|
||
<option value="all">Любой приоритет</option>
|
||
<option value="high">Высокий</option>
|
||
<option value="medium">Средний</option>
|
||
<option value="low">Низкий</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 80px">№</th>
|
||
<th>Тема</th>
|
||
<th style="width: 120px">Статус</th>
|
||
<th style="width: 100px">Приоритет</th>
|
||
<th style="width: 120px">Дата</th>
|
||
<th style="width: 140px">Исполнитель</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tickets-body"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="empty" id="empty-state" style="display: none">
|
||
<div class="empty-icon">📭</div>
|
||
<div style="font-size: 16px; font-weight: var(--kt-ai-weight-medium)">Нет заявок</div>
|
||
<div style="font-size: 14px; margin-top: var(--kt-ai-space-2)">По выбранным фильтрам ничего не найдено</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<div class="drawer-overlay" id="drawer-overlay" onclick="closeDrawer()"></div>
|
||
<div class="drawer" id="drawer">
|
||
<div class="drawer-header">
|
||
<h2 id="drawer-title">Заявка №000</h2>
|
||
<button class="drawer-close" onclick="closeDrawer()">×</button>
|
||
</div>
|
||
<div class="drawer-body" id="drawer-body"></div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="new-ticket-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h2 style="margin: 0; font-size: 18px">Новая заявка</h2>
|
||
<button class="btn-icon" onclick="closeNewTicketModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="kt-ai-field">
|
||
<label>Тема *</label>
|
||
<input class="kt-ai-input" id="new-subject" placeholder="Краткое описание проблемы" style="width: 100%">
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Описание *</label>
|
||
<textarea class="kt-ai-textarea" id="new-description" rows="4" placeholder="Подробное описание..." style="width: 100%"></textarea>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Категория</label>
|
||
<select class="kt-ai-select" id="new-category" style="width: 100%">
|
||
<option value="technics">Технические вопросы</option>
|
||
<option value="billing">Биллинг и оплата</option>
|
||
<option value="connection">Подключение услуг</option>
|
||
<option value="other">Другое</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Приоритет</label>
|
||
<select class="kt-ai-select" id="new-priority" style="width: 100%">
|
||
<option value="low">Низкий</option>
|
||
<option value="medium" selected>Средний</option>
|
||
<option value="high">Высокий</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<label>Контактный телефон</label>
|
||
<input class="kt-ai-input" id="new-phone" placeholder="+7 (___) ___-__-__" style="width: 100%">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="closeNewTicketModal()">Отмена</button>
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="createTicket()">Создать заявку</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DEMO_TICKETS = [
|
||
{ id: 1001, subject: "Не работает интернет", category: "technics", status: "progress", priority: "high", date: "2026-09-14", executor: "Алиев М.", description: "Полное отсутствие связи с утра. Роутер перезагружал." },
|
||
{ id: 1002, subject: "Ошибка в счёте", category: "billing", status: "new", priority: "medium", date: "2026-09-14", executor: "—", description: "В счёте за август указана неверная сумма." },
|
||
{ id: 1003, subject: "Подключить ТВ", category: "connection", status: "done", priority: "low", date: "2026-09-13", executor: "Иванов А.", description: "Заявка на подключение цифрового ТВ." },
|
||
{ id: 1004, subject: "Низкая скорость", category: "technics", status: "progress", priority: "medium", date: "2026-09-13", executor: "Алиев М.", description: "Скорость не превышает 5 Мбит/с." },
|
||
{ id: 1005, subject: "Смена тарифа", category: "connection", status: "done", priority: "low", date: "2026-09-12", executor: "Петров К.", description: "Переход на тариф «Домашний»." },
|
||
{ id: 1006, subject: "Нет связи", category: "technics", status: "overdue", priority: "high", date: "2026-09-11", executor: "—", description: "Нет телефонной связи и интернета." },
|
||
{ id: 1007, subject: "Перерасчёт", category: "billing", status: "new", priority: "medium", date: "2026-09-12", executor: "—", description: "Запрошен перерасчёт за период отпуска." },
|
||
{ id: 1008, subject: "Настройка роутера", category: "technics", status: "done", priority: "low", date: "2026-09-10", executor: "Сидоров В.", description: "Помощь в настройке Wi-Fi роутера." },
|
||
{ id: 1009, subject: "Отключение услуги", category: "connection", status: "progress", priority: "medium", date: "2026-09-11", executor: "Иванов А.", description: "Отключить услугу «Антивирус»." },
|
||
{ id: 1010, subject: "Дублирование платежа", category: "billing", status: "done", priority: "high", date: "2026-09-09", executor: "Петров К.", description: "Платёж проведён дважды." },
|
||
{ id: 1011, subject: "Помехи на линии", category: "technics", status: "new", priority: "medium", date: "2026-09-14", executor: "—", description: "Постоянные помехи при разговоре." },
|
||
{ id: 1012, subject: "Запрос акта", category: "billing", status: "done", priority: "low", date: "2026-09-08", executor: "Иванов А.", description: "Нужен акт выполненных работ." },
|
||
{ id: 1013, subject: "Перенос срока оплаты", category: "billing", status: "progress", priority: "medium", date: "2026-09-10", executor: "Алиев М.", description: "Запрос на отсрочку платежа." },
|
||
{ id: 1014, subject: "Обрыв кабеля", category: "technics", status: "overdue", priority: "high", date: "2026-09-09", executor: "—", description: "Видно повреждение кабеля в подъезде." },
|
||
{ id: 1015, subject: "Подключение SIP", category: "connection", status: "new", priority: "low", date: "2026-09-13", executor: "—", description: "Интересует SIP-телефония для офиса." },
|
||
{ id: 1016, subject: "Не приходит SMS", category: "technics", status: "done", priority: "medium", date: "2026-09-07", executor: "Сидоров В.", description: "Не приходят SMS с кодами." },
|
||
{ id: 1017, subject: "Возврат средств", category: "billing", status: "progress", priority: "high", date: "2026-09-11", executor: "Петров К.", description: "Запрос на возврат переплаты." },
|
||
{ id: 1018, subject: "Частые обрывы", category: "technics", status: "new", priority: "high", date: "2026-09-14", executor: "—", description: "Связь пропадает несколько раз в день." },
|
||
{ id: 1019, subject: "Смена собственника", category: "billing", status: "done", priority: "medium", date: "2026-09-06", executor: "Иванов А.", description: "Обновление данных собственника." },
|
||
{ id: 1020, subject: "Тест скорости", category: "technics", status: "done", priority: "low", date: "2026-09-05", executor: "Алиев М.", description: "Проверка соответствия скорости тарифу." },
|
||
{ id: 1021, subject: "Отключить телефон", category: "connection", status: "progress", priority: "low", date: "2026-09-12", executor: "Сидоров В.", description: "Отключение домашнего телефона." },
|
||
{ id: 1022, subject: "Ошибка авторизации", category: "technics", status: "new", priority: "medium", date: "2026-09-13", executor: "—", description: "Не входит в личный кабинет." },
|
||
{ id: 1023, subject: "Подключение статического IP", category: "connection", status: "done", priority: "medium", date: "2026-09-04", executor: "Петров К.", description: "Заказ услуги статического IP." },
|
||
{ id: 1024, subject: "Некорректный счёт", category: "billing", status: "overdue", priority: "high", date: "2026-09-08", executor: "—", description: "В счёте лишние услуги." },
|
||
{ id: 1025, subject: "Консультация по тарифам", category: "other", status: "done", priority: "low", date: "2026-09-03", executor: "Иванов А.", description: "Нужна консультация по новым тарифам." }
|
||
];
|
||
|
||
let tickets = [];
|
||
let currentFilters = { status: 'all', priority: 'all' };
|
||
|
||
function init() {
|
||
loadTickets();
|
||
render();
|
||
setupFilters();
|
||
}
|
||
|
||
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 setupFilters() {
|
||
document.querySelectorAll('[data-filter="status"]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('[data-filter="status"]').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
currentFilters.status = btn.dataset.value;
|
||
render();
|
||
});
|
||
});
|
||
document.getElementById('priority-filter').addEventListener('change', (e) => {
|
||
currentFilters.priority = e.target.value;
|
||
render();
|
||
});
|
||
}
|
||
|
||
function render() {
|
||
const filtered = getFilteredTickets();
|
||
renderKPI(filtered);
|
||
renderChart(filtered);
|
||
renderTable(filtered);
|
||
document.getElementById('empty-state').style.display = filtered.length === 0 ? 'block' : 'none';
|
||
}
|
||
|
||
function getFilteredTickets() {
|
||
return tickets.filter(t => {
|
||
if (currentFilters.status !== 'all' && t.status !== currentFilters.status) return false;
|
||
if (currentFilters.priority !== 'all' && t.priority !== currentFilters.priority) return false;
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderKPI(data) {
|
||
const total = data.length;
|
||
const progress = data.filter(t => t.status === 'progress' || t.status === 'new').length;
|
||
const done = data.filter(t => t.status === 'done').length;
|
||
const overdue = data.filter(t => t.status === 'overdue').length;
|
||
|
||
document.getElementById('kpi-total').textContent = total;
|
||
document.getElementById('kpi-progress').textContent = progress;
|
||
document.getElementById('kpi-done').textContent = done;
|
||
document.getElementById('kpi-overdue').textContent = overdue;
|
||
}
|
||
|
||
function renderChart(data) {
|
||
const days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
|
||
const counts = [0, 0, 0, 0, 0, 0, 0];
|
||
|
||
data.forEach(t => {
|
||
const date = new Date(t.date);
|
||
const dayIndex = (date.getDay() + 6) % 7;
|
||
counts[dayIndex]++;
|
||
});
|
||
|
||
const max = Math.max(...counts, 1);
|
||
const chartEl = document.getElementById('chart');
|
||
const labelsEl = document.getElementById('chart-labels');
|
||
|
||
chartEl.innerHTML = '';
|
||
labelsEl.innerHTML = '';
|
||
|
||
counts.forEach((count, i) => {
|
||
const height = (count / max) * 160;
|
||
const bar = document.createElement('div');
|
||
bar.className = 'chart-bar';
|
||
bar.style.height = height + 'px';
|
||
bar.setAttribute('data-kt-tip', count + ' заявок');
|
||
chartEl.appendChild(bar);
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'chart-label';
|
||
label.textContent = days[i];
|
||
labelsEl.appendChild(label);
|
||
});
|
||
}
|
||
|
||
function renderTable(data) {
|
||
const tbody = document.getElementById('tickets-body');
|
||
tbody.innerHTML = '';
|
||
|
||
data.forEach(t => {
|
||
const tr = document.createElement('tr');
|
||
tr.onclick = () => openDrawer(t.id);
|
||
|
||
const statusText = { new: 'Новый', progress: 'В работе', done: 'Решено', overdue: 'Просрочено' }[t.status];
|
||
|
||
const priorityHtml = `
|
||
<div class="priority">
|
||
<div class="priority-dot ${t.priority === 'high' ? 'active high' : ''}"></div>
|
||
<div class="priority-dot ${t.priority === 'medium' ? 'active medium' : ''}"></div>
|
||
<div class="priority-dot ${t.priority === 'low' ? 'active low' : ''}"></div>
|
||
</div>
|
||
`;
|
||
|
||
tr.innerHTML = `
|
||
<td style="font-weight: var(--kt-ai-weight-medium)">#${t.id}</td>
|
||
<td>${t.subject}</td>
|
||
<td><span class="status ${t.status}">${statusText}</span></td>
|
||
<td>${priorityHtml}</td>
|
||
<td style="color: var(--kt-ai-fg-muted)">${formatDate(t.date)}</td>
|
||
<td style="color: var(--kt-ai-fg-muted)">${t.executor}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
const date = new Date(dateStr);
|
||
const today = new Date();
|
||
const diff = Math.floor((today - date) / (1000 * 60 * 60 * 24));
|
||
|
||
if (diff === 0) return 'Сегодня';
|
||
if (diff === 1) return 'Вчера';
|
||
if (diff < 7) return date.toLocaleDateString('ru-RU', { weekday: 'short' });
|
||
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
||
}
|
||
|
||
function openDrawer(id) {
|
||
const ticket = tickets.find(t => t.id === id);
|
||
if (!ticket) return;
|
||
|
||
document.getElementById('drawer-title').textContent = `Заявка №${ticket.id}`;
|
||
|
||
const statusText = { new: 'Новый', progress: 'В работе', done: 'Решено', overdue: 'Просрочено' }[ticket.status];
|
||
const categoryText = { technics: 'Технические вопросы', billing: 'Биллинг и оплата', connection: 'Подключение услуг', other: 'Другое' }[ticket.category];
|
||
const priorityText = { low: 'Низкий', medium: 'Средний', high: 'Высокий' }[ticket.priority];
|
||
|
||
document.getElementById('drawer-body').innerHTML = `
|
||
<div class="detail-row">
|
||
<span class="detail-label">Тема</span>
|
||
<span class="detail-value">${ticket.subject}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Статус</span>
|
||
<span class="detail-value"><span class="status ${ticket.status}">${statusText}</span></span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Категория</span>
|
||
<span class="detail-value">${categoryText}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Приоритет</span>
|
||
<span class="detail-value">${priorityText}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Дата создания</span>
|
||
<span class="detail-value">${ticket.date}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">Исполнитель</span>
|
||
<span class="detail-value">${ticket.executor}</span>
|
||
</div>
|
||
<div style="margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-4); border-top: 1px solid var(--kt-ai-border)">
|
||
<div style="font-size: 13px; font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-2)">Описание</div>
|
||
<div style="font-size: 14px; line-height: 1.6">${ticket.description}</div>
|
||
</div>
|
||
<div style="margin-top: var(--kt-ai-space-6); display: flex; gap: var(--kt-ai-space-3)">
|
||
<button class="kt-ai-btn" data-variant="primary" style="flex: 1" onclick="changeStatus(${ticket.id}, 'done')">✓ Решено</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" style="flex: 1" onclick="changeStatus(${ticket.id}, 'progress')">⟳ В работу</button>
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('drawer').classList.add('open');
|
||
document.getElementById('drawer-overlay').classList.add('open');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById('drawer').classList.remove('open');
|
||
document.getElementById('drawer-overlay').classList.remove('open');
|
||
}
|
||
|
||
function changeStatus(id, newStatus) {
|
||
const ticket = tickets.find(t => t.id === id);
|
||
if (ticket) {
|
||
ticket.status = newStatus;
|
||
saveTickets();
|
||
render();
|
||
closeDrawer();
|
||
}
|
||
}
|
||
|
||
function openNewTicketModal() {
|
||
document.getElementById('new-subject').value = '';
|
||
document.getElementById('new-description').value = '';
|
||
document.getElementById('new-category').value = 'technics';
|
||
document.getElementById('new-priority').value = 'medium';
|
||
document.getElementById('new-phone').value = '';
|
||
document.getElementById('new-ticket-modal').classList.add('open');
|
||
}
|
||
|
||
function closeNewTicketModal() {
|
||
document.getElementById('new-ticket-modal').classList.remove('open');
|
||
}
|
||
|
||
function createTicket() {
|
||
const subject = document.getElementById('new-subject').value.trim();
|
||
const description = document.getElementById('new-description').value.trim();
|
||
|
||
if (!subject || !description) {
|
||
alert('Заполните тему и описание');
|
||
return;
|
||
}
|
||
|
||
const newTicket = {
|
||
id: Math.max(...tickets.map(t => t.id)) + 1,
|
||
subject: subject,
|
||
category: document.getElementById('new-category').value,
|
||
status: 'new',
|
||
priority: document.getElementById('new-priority').value,
|
||
date: new Date().toISOString().split('T')[0],
|
||
executor: '—',
|
||
description: description
|
||
};
|
||
|
||
tickets.unshift(newTicket);
|
||
saveTickets();
|
||
render();
|
||
closeNewTicketModal();
|
||
|
||
openDrawer(newTicket.id);
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|