glubokoe-intervyu/index.html

452 lines
20 KiB
HTML
Raw Permalink 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">
<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>
.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; }
.app-header { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) 0; }
.app-logo { height: 32px; }
.app-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); }
.main-content { flex: 1; padding: var(--kt-ai-space-6) 0; }
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.kt-ai-kpi { 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); }
.kt-ai-kpi .value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
.kt-ai-kpi .label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
.kt-ai-kpi .hint { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
.content-grid { display: grid; grid-template-columns: 1fr 320px; gap: var(--kt-ai-space-6); }
.table-card { 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); }
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-4); }
.table-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); }
.filters { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; }
.kt-ai-table-wrap { overflow-x: auto; }
.kt-ai-table { width: 100%; border-collapse: collapse; }
.kt-ai-table th { text-align: left; font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg-muted); padding: var(--kt-ai-space-3); border-bottom: 1px solid var(--kt-ai-border); }
.kt-ai-table td { padding: var(--kt-ai-space-3); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg); border-bottom: 1px solid var(--kt-ai-border-strong); }
.kt-ai-table tr:hover td { background: var(--kt-ai-bg-soft); }
.status-chip { display: inline-flex; align-items: center; padding: var(--kt-ai-space-1) var(--kt-ai-space-2); border-radius: var(--kt-ai-radius-md); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-new { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
.status-progress { background: var(--kt-ai-status-warn-bg); color: var(--kt-ai-status-warn-fg); }
.status-done { background: var(--kt-ai-status-success-bg); color: var(--kt-ai-status-success-fg); }
.status-cancelled { background: var(--kt-ai-status-risk-bg); color: var(--kt-ai-status-risk-fg); }
.form-card { 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); position: sticky; top: var(--kt-ai-space-6); }
.form-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.kt-ai-field { margin-bottom: var(--kt-ai-space-4); }
.kt-ai-field label { display: block; font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); color: var(--kt-ai-fg); margin-bottom: var(--kt-ai-space-2); }
.kt-ai-input, .kt-ai-select, .kt-ai-textarea { width: 100%; padding: var(--kt-ai-space-2) var(--kt-ai-space-3); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-md); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: var(--kt-ai-text-sm); }
.kt-ai-input:focus, .kt-ai-select:focus, .kt-ai-textarea:focus { outline: 2px solid var(--kt-ai-primary); border-color: transparent; }
.kt-ai-textarea { min-height: 100px; resize: vertical; }
.chart-container { margin-top: var(--kt-ai-space-6); padding: var(--kt-ai-space-5); background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); }
.chart-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.chart-svg { width: 100%; height: 200px; }
.chart-bar { transition: opacity 0.2s; }
.chart-bar:hover { opacity: 0.7; }
.empty-state { 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); }
.empty-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); }
.empty-desc { font-size: var(--kt-ai-text-sm); }
.toast { position: fixed; bottom: var(--kt-ai-space-6); right: var(--kt-ai-space-6); background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); box-shadow: 0 8px 32px rgba(0,0,0,0.12); z-index: 1000; transform: translateY(100px); opacity: 0; transition: transform 0.3s, opacity 0.3s; }
.toast.show { transform: translateY(0); opacity: 1; }
.toast-message { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg); }
</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="app-header">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" class="app-logo">
<span class="app-title">Управление заявками</span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
</div>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="resetDataBtn">Сбросить данные</button>
</div>
</header>
<main class="main-content">
<div class="kt-ai-wrap">
<div class="kpi-strip" id="kpiStrip">
<div class="kt-ai-kpi">
<div class="value" id="kpiTotal">0</div>
<div class="label">Всего заявок</div>
<div class="hint">за все время</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpiActive">0</div>
<div class="label">В работе</div>
<div class="hint">новые и активные</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpiCompleted">0</div>
<div class="label">Завершено</div>
<div class="hint">успешно закрыто</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpiAvgTime">0</div>
<div class="label">Ср. время</div>
<div class="hint">дней на выполнение</div>
</div>
</div>
<div class="content-grid">
<div class="table-section">
<div class="table-card">
<div class="table-header">
<div class="table-title">Заявки</div>
<div class="filters">
<select class="kt-ai-select" id="statusFilter" data-kt-filter="status">
<option value="">Все статусы</option>
<option value="new">Новые</option>
<option value="in_progress">В работе</option>
<option value="completed">Завершено</option>
<option value="cancelled">Отменено</option>
</select>
<select class="kt-ai-select" id="periodFilter" data-kt-filter="period">
<option value="7">7 дней</option>
<option value="14">14 дней</option>
<option value="30">30 дней</option>
<option value="all">Все время</option>
</select>
</div>
</div>
<div class="kt-ai-table-wrap" id="tableWrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>ID</th>
<th>Тема</th>
<th>Заявитель</th>
<th>Дата</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-icon">📭</div>
<div class="empty-title">Нет данных</div>
<div class="empty-desc">По выбранным фильтрам заявок не найдено</div>
</div>
</div>
<div class="chart-container">
<div class="chart-title">Заявки по дням</div>
<svg class="chart-svg" id="chartSvg" viewBox="0 0 600 200"></svg>
</div>
</div>
<div class="form-section">
<div class="form-card">
<div class="form-title">Новая заявка</div>
<form id="createForm">
<div class="kt-ai-field">
<label for="subject">Тема</label>
<input type="text" class="kt-ai-input" id="subject" required placeholder="Краткое описание">
</div>
<div class="kt-ai-field">
<label for="applicant">Заявитель</label>
<input type="text" class="kt-ai-input" id="applicant" required placeholder="ФИО или отдел">
</div>
<div class="kt-ai-field">
<label for="description">Описание</label>
<textarea class="kt-ai-textarea" id="description" required placeholder="Подробности задачи"></textarea>
</div>
<div class="kt-ai-field">
<label for="priority">Приоритет</label>
<select class="kt-ai-select" id="priority">
<option value="low">Низкий</option>
<option value="normal" selected>Обычный</option>
<option value="high">Высокий</option>
<option value="urgent">Срочный</option>
</select>
</div>
<button type="submit" class="kt-ai-btn" data-variant="primary" data-size="lg" style="width: 100%;">Создать заявку</button>
</form>
</div>
</div>
</div>
</div>
</main>
<div class="toast" id="toast">
<div class="toast-message" id="toastMessage"></div>
</div>
<script>
const STORAGE_KEY = 'tickets_data_v1';
const defaultData = {
tickets: [
{ id: 1001, subject: 'Настройка рабочего места', applicant: 'Иванов А.С.', description: 'Установка ПО для нового сотрудника', priority: 'normal', status: 'completed', createdAt: '2024-01-15', completedAt: '2024-01-16' },
{ id: 1002, subject: 'Ремонт принтера', applicant: 'Отдел продаж', description: 'Замятие бумаги, требуется ремонт', priority: 'high', status: 'in_progress', createdAt: '2024-01-18', completedAt: null },
{ id: 1003, subject: 'Доступ к CRM', applicant: 'Петрова Е.В.', description: 'Запрос прав доступа к системе', priority: 'normal', status: 'new', createdAt: '2024-01-19', completedAt: null },
{ id: 1004, subject: 'Замена монитора', applicant: 'Сидоров К.М.', description: 'Мерцание экрана, артефакты', priority: 'low', status: 'cancelled', createdAt: '2024-01-17', completedAt: null },
{ id: 1005, subject: 'Подключение телефона', applicant: 'Бухгалтерия', description: 'Добавить внутренний номер', priority: 'normal', status: 'completed', createdAt: '2024-01-20', completedAt: '2024-01-20' },
{ id: 1006, subject: 'Обновление антивируса', applicant: 'IT отдел', description: 'Плановое обновление баз', priority: 'high', status: 'in_progress', createdAt: '2024-01-21', completedAt: null },
{ id: 1007, subject: 'Настройка VPN', applicant: 'Ахметов Р.Б.', description: 'Удалённый доступ для работы из дома', priority: 'urgent', status: 'new', createdAt: '2024-01-22', completedAt: null },
]
};
let data = loadData();
function loadData() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
return JSON.parse(stored);
} catch (e) {
return defaultData;
}
}
return defaultData;
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function resetData() {
localStorage.removeItem(STORAGE_KEY);
data = defaultData;
renderAll();
showToast('Данные сброшены к демо-версии');
}
function generateId() {
const max = data.tickets.reduce((max, t) => Math.max(max, t.id), 1000);
return max + 1;
}
function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
}
function getStatusLabel(status) {
const labels = { new: 'Новая', in_progress: 'В работе', completed: 'Завершено', cancelled: 'Отменено' };
return labels[status] || status;
}
function getStatusClass(status) {
const classes = { new: 'status-new', in_progress: 'status-progress', completed: 'status-done', cancelled: 'status-cancelled' };
return classes[status] || '';
}
function getFilteredTickets() {
const statusFilter = document.getElementById('statusFilter').value;
const periodFilter = document.getElementById('periodFilter').value;
let filtered = [...data.tickets];
if (statusFilter) {
filtered = filtered.filter(t => t.status === statusFilter);
}
if (periodFilter !== 'all') {
const days = parseInt(periodFilter);
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
filtered = filtered.filter(t => new Date(t.createdAt) >= cutoff);
}
return filtered.sort((a, b) => b.id - a.id);
}
function renderKPIs() {
const tickets = data.tickets;
const total = tickets.length;
const active = tickets.filter(t => t.status === 'new' || t.status === 'in_progress').length;
const completed = tickets.filter(t => t.status === 'completed').length;
const completedTickets = tickets.filter(t => t.status === 'completed' && t.completedAt);
let avgDays = 0;
if (completedTickets.length > 0) {
const totalDays = completedTickets.reduce((sum, t) => {
const start = new Date(t.createdAt);
const end = new Date(t.completedAt);
return sum + (end - start) / (1000 * 60 * 60 * 24);
}, 0);
avgDays = totalDays / completedTickets.length;
}
document.getElementById('kpiTotal').textContent = total;
document.getElementById('kpiActive').textContent = active;
document.getElementById('kpiCompleted').textContent = completed;
document.getElementById('kpiAvgTime').textContent = avgDays.toFixed(1);
}
function renderTable() {
const filtered = getFilteredTickets();
const tbody = document.getElementById('tableBody');
const tableWrap = document.getElementById('tableWrap');
const emptyState = document.getElementById('emptyState');
if (filtered.length === 0) {
tableWrap.style.display = 'none';
emptyState.style.display = 'block';
return;
}
tableWrap.style.display = 'block';
emptyState.style.display = 'none';
tbody.innerHTML = filtered.map(t => `
<tr>
<td style="font-family: var(--kt-ai-font-mono);">${t.id}</td>
<td>${t.subject}</td>
<td>${t.applicant}</td>
<td>${formatDate(t.createdAt)}</td>
<td><span class="status-chip ${getStatusClass(t.status)}">${getStatusLabel(t.status)}</span></td>
<td>
${t.status === 'new' ? `<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="updateStatus(${t.id}, 'in_progress')">В работу</button>` : ''}
${t.status === 'in_progress' ? `<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="updateStatus(${t.id}, 'completed')">Завершить</button>` : ''}
${t.status === 'new' || t.status === 'in_progress' ? `<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="updateStatus(${t.id}, 'cancelled')">Отменить</button>` : ''}
</td>
</tr>
`).join('');
}
function updateStatus(id, status) {
const ticket = data.tickets.find(t => t.id === id);
if (ticket) {
ticket.status = status;
if (status === 'completed') {
ticket.completedAt = new Date().toISOString().split('T')[0];
} else {
ticket.completedAt = null;
}
saveData();
renderAll();
showToast(`Заявка #${id}: ${getStatusLabel(status)}`);
}
}
function renderChart() {
const svg = document.getElementById('chartSvg');
const periodFilter = document.getElementById('periodFilter').value;
const days = periodFilter === 'all' ? 30 : parseInt(periodFilter);
const counts = {};
const today = new Date();
for (let i = days - 1; i >= 0; i--) {
const d = new Date(today);
d.setDate(d.getDate() - i);
const key = d.toISOString().split('T')[0];
counts[key] = 0;
}
data.tickets.forEach(t => {
if (counts.hasOwnProperty(t.createdAt)) {
counts[t.createdAt]++;
}
});
const labels = Object.keys(counts);
const values = Object.values(counts);
const max = Math.max(...values, 1);
const barWidth = 24;
const gap = 8;
const chartWidth = labels.length * (barWidth + gap);
const chartHeight = 180;
const padding = { top: 10, right: 10, bottom: 30, left: 30 };
let svgContent = `<rect width="100%" height="100%" fill="none"/>`;
labels.forEach((label, i) => {
const x = padding.left + i * (barWidth + gap);
const h = (values[i] / max) * (chartHeight - padding.bottom);
const y = chartHeight - padding.bottom - h;
const date = new Date(label);
const shortLabel = date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
svgContent += `
<rect class="chart-bar" x="${x}" y="${y}" width="${barWidth}" height="${h}"
fill="var(--kt-ai-chart-blue)" rx="2"
data-kt-tip="${values[i]} заявок (${shortLabel})">
<title>${values[i]} заявок — ${shortLabel}</title>
</rect>
<text x="${x + barWidth/2}" y="${chartHeight - 8}"
text-anchor="middle" font-size="9" fill="var(--kt-ai-fg-muted)">
${date.getDate()}.${date.getMonth()+1}
</text>
`;
});
svg.innerHTML = svgContent;
}
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
function renderAll() {
renderKPIs();
renderTable();
renderChart();
}
document.getElementById('createForm').addEventListener('submit', function(e) {
e.preventDefault();
const subject = document.getElementById('subject').value.trim();
const applicant = document.getElementById('applicant').value.trim();
const description = document.getElementById('description').value.trim();
const priority = document.getElementById('priority').value;
if (!subject || !applicant || !description) {
showToast('Заполните все обязательные поля');
return;
}
const ticket = {
id: generateId(),
subject,
applicant,
description,
priority,
status: 'new',
createdAt: new Date().toISOString().split('T')[0],
completedAt: null
};
data.tickets.push(ticket);
saveData();
this.reset();
renderAll();
showToast(`Заявка #${ticket.id} создана`);
});
document.getElementById('statusFilter').addEventListener('change', renderAll);
document.getElementById('periodFilter').addEventListener('change', renderAll);
document.getElementById('resetDataBtn').addEventListener('click', function() {
if (confirm('Сбросить все данные к демо-версии?')) {
resetData();
}
});
renderAll();
</script>
</body>
</html>