pasport-prochital-ya-vybral/index.html

476 lines
25 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">
<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; background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
.kt-ai-header-content { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
.kt-ai-logo-wrap { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
.kt-ai-main { padding: var(--kt-ai-space-6) 0; }
.kt-ai-kpi-strip { display: grid; grid-template-columns: repeat(4, 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); }
.kt-ai-filters { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-5); align-items: center; }
.kt-ai-filter-group { display: flex; flex-direction: column; gap: var(--kt-ai-space-1); }
.kt-ai-filter-label { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.kt-ai-table-wrap { overflow-x: auto; background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); }
.kt-ai-table { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-sm); }
.kt-ai-table th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium); }
.kt-ai-table td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border-strong); vertical-align: middle; }
.kt-ai-table tr:last-child td { border-bottom: none; }
.kt-ai-table tr:hover td { background: var(--kt-ai-bg-soft); }
.kt-ai-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.kt-ai-empty { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
.kt-ai-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.kt-ai-modal-overlay.in { display: flex; }
.kt-ai-modal { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); max-width: 560px; width: 90%; max-height: 90vh; overflow-y: auto; }
.kt-ai-modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-5); }
.kt-ai-modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.kt-ai-modal-close { background: none; border: none; font-size: var(--kt-ai-text-2xl); color: var(--kt-ai-fg-muted); cursor: pointer; padding: 0; line-height: 1; }
.kt-ai-modal-close:hover { color: var(--kt-ai-fg); }
.kt-ai-modal-body { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); }
.kt-ai-info-row { display: flex; justify-content: space-between; padding: var(--kt-ai-space-2) 0; border-bottom: 1px solid var(--kt-ai-border-strong); }
.kt-ai-info-row:last-of-type { border-bottom: none; }
.kt-ai-info-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.kt-ai-info-value { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
.kt-ai-modal-actions { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); }
.kt-ai-decision-note { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
@media (max-width: 768px) {
.kt-ai-kpi-strip { grid-template-columns: repeat(2, 1fr); }
.kt-ai-filters { flex-direction: column; align-items: stretch; }
.kt-ai-filter-group { width: 100%; }
.kt-ai-select { 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="kt-ai-header-content">
<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>
</div>
</header>
<main class="kt-ai-wrap kt-ai-main">
<section class="kt-ai-kpi-strip">
<div class="kt-ai-kpi">
<div class="value" id="kpi-total">0</div>
<div class="label">Всего заявок</div>
<div class="hint">за выбранный период</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-review">0</div>
<div class="label">На рассмотрении</div>
<div class="hint">требуют решения</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-approved" style="color: var(--kt-ai-status-ok-fg)">0</div>
<div class="label">Принято</div>
<div class="hint">за выбранный период</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-rejected" style="color: var(--kt-ai-status-risk-fg)">0</div>
<div class="label">Отклонено</div>
<div class="hint">за выбранный период</div>
</div>
</section>
<section class="kt-ai-filters">
<div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Период</label>
<select class="kt-ai-select" id="filter-period" style="min-width: 140px">
<option value="7d">7 дней</option>
<option value="30d">30 дней</option>
<option value="quarter">Квартал</option>
<option value="year">Год</option>
</select>
</div>
<div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Статус</label>
<select class="kt-ai-select" id="filter-status" style="min-width: 160px">
<option value="all">Все статусы</option>
<option value="new">Новый</option>
<option value="review">На рассмотрении</option>
<option value="decided">Решение принято</option>
<option value="rejected">Отклонено</option>
</select>
</div>
<div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Тип заявки</label>
<select class="kt-ai-select" id="filter-type" style="min-width: 160px">
<option value="all">Все типы</option>
<option value="access">Доступ к данным</option>
<option value="integration">Интеграция</option>
<option value="hardware">Оборудование</option>
<option value="other">Другое</option>
</select>
</div>
<div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Приоритет</label>
<select class="kt-ai-select" id="filter-priority" style="min-width: 140px">
<option value="all">Все приоритеты</option>
<option value="high">Высокий</option>
<option value="medium">Средний</option>
<option value="low">Низкий</option>
</select>
</div>
</section>
<section class="kt-ai-table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>Дата</th>
<th>Заявитель</th>
<th>Тип</th>
<th>Приоритет</th>
<th>Статус</th>
<th>Решение</th>
<th></th>
</tr>
</thead>
<tbody id="requests-table-body">
</tbody>
</table>
<div id="empty-state" class="kt-ai-empty" style="display: none">
<p>Нет данных за выбранный период</p>
<p style="font-size: var(--kt-ai-text-xs); margin-top: var(--kt-ai-space-2)">Измените параметры фильтра</p>
</div>
</section>
</main>
<div class="kt-ai-modal-overlay" id="modal-overlay">
<div class="kt-ai-modal">
<div class="kt-ai-modal-head">
<h2 class="kt-ai-modal-title">Рассмотрение заявки</h2>
<button class="kt-ai-modal-close" id="modal-close">&times;</button>
</div>
<div class="kt-ai-modal-body">
<div id="request-details"></div>
<div class="kt-ai-field">
<label for="decision-comment">Комментарий к решению</label>
<textarea id="decision-comment" class="kt-ai-textarea" rows="3" placeholder="Обоснование решения..."></textarea>
</div>
<div class="kt-ai-modal-actions">
<button class="kt-ai-btn" data-variant="primary" id="btn-approve">Принять</button>
<button class="kt-ai-btn" data-variant="danger" id="btn-reject">Отклонить</button>
<button class="kt-ai-btn" data-variant="ghost" id="btn-rework">На доработку</button>
</div>
<p class="kt-ai-decision-note">Решение будет сохранено в localStorage</p>
</div>
</div>
</div>
<script>
// Реестр фильтров
const FILTERS = [
{ id: 'period', label: 'Период', affects: ['table', 'kpi', 'empty'] },
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'empty'] },
{ id: 'type', label: 'Тип', affects: ['table', 'kpi', 'empty'] },
{ id: 'priority', label: 'Приоритет', affects: ['table', 'kpi', 'empty'] }
];
// Демо-данные
const DEMO_DATA = {
'7d': [
{ id: 1, date: '2026-09-10', applicant: 'Иванов А.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 2, date: '2026-09-11', applicant: 'Петрова М.К.', department: 'Отдел аналитики', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null },
{ id: 3, date: '2026-09-12', applicant: 'Сидоров Е.В.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Согласовано в рамках бюджета' },
{ id: 4, date: '2026-09-13', applicant: 'Алиев Н.Р.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 5, date: '2026-09-14', applicant: 'Козлова Д.А.', department: 'Бухгалтерия', type: 'other', typeName: 'Другое', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null }
],
'30d': [
{ id: 6, date: '2026-09-01', applicant: 'Нурсултанов Б.Т.', department: 'Отдел маркетинга', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция одобрена' },
{ id: 7, date: '2026-09-03', applicant: 'Омарова Г.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ предоставлен' },
{ id: 8, date: '2026-09-05', applicant: 'Васильев П.И.', department: 'Служба поддержки', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 9, date: '2026-09-07', applicant: 'Жанарбеков А.К.', department: 'Отдел продаж', type: 'other', typeName: 'Другое', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null },
{ id: 10, date: '2026-09-09', applicant: 'Смирнова О.Л.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 11, date: '2026-09-10', applicant: 'Иванов А.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 12, date: '2026-09-11', applicant: 'Петрова М.К.', department: 'Отдел аналитики', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null },
{ id: 13, date: '2026-09-12', applicant: 'Сидоров Е.В.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Согласовано в рамках бюджета' }
],
'quarter': [
{ id: 14, date: '2026-07-15', applicant: 'Ахметов Р.Б.', department: 'Департамент ИТ', type: 'integration', typeName: 'Интеграция', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция завершена' },
{ id: 15, date: '2026-08-01', applicant: 'Кузнецова А.В.', department: 'Отдел кадров', type: 'access', typeName: 'Доступ к данным', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ настроен' },
{ id: 16, date: '2026-08-10', applicant: 'Токтаев К.С.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Оборудование закуплено' },
{ id: 17, date: '2026-08-20', applicant: 'Лебедева М.А.', department: 'Бухгалтерия', type: 'other', typeName: 'Другое', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 18, date: '2026-09-01', applicant: 'Нурсултанов Б.Т.', department: 'Отдел маркетинга', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция одобрена' },
{ id: 19, date: '2026-09-05', applicant: 'Васильев П.И.', department: 'Служба поддержки', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null }
],
'year': [] // Пустой срез для демонстрации пустого состояния
};
// Загрузка сохранённых решений из localStorage
function loadSavedDecisions() {
const saved = localStorage.getItem('requestDecisions');
return saved ? JSON.parse(saved) : {};
}
// Сохранение решения в localStorage
function saveDecision(requestId, decision, comment) {
const decisions = loadSavedDecisions();
decisions[requestId] = { decision, comment, date: new Date().toISOString() };
localStorage.setItem('requestDecisions', JSON.stringify(decisions));
}
// Применение сохранённых решений к демо-данным
function applySavedDecisions(data) {
const decisions = loadSavedDecisions();
return data.map(item => {
if (decisions[item.id]) {
const saved = decisions[item.id];
return {
...item,
decision: saved.decision,
decisionName: saved.decision === 'approved' ? 'Принято' : saved.decision === 'rejected' ? 'Отклонено' : 'На доработку',
status: 'decided',
statusName: 'Решение принято',
comment: saved.comment
};
}
return item;
});
}
// Получение данных по фильтру периода
function getDataForPeriod(period) {
const baseData = DEMO_DATA[period] || [];
return applySavedDecisions(baseData);
}
// Фильтрация данных
function filterData(data, filters) {
let filtered = [...data];
if (filters.status !== 'all') {
if (filters.status === 'new') {
filtered = filtered.filter(item => item.status === 'new');
} else if (filters.status === 'review') {
filtered = filtered.filter(item => item.status === 'review');
} else if (filters.status === 'decided') {
filtered = filtered.filter(item => item.status === 'decided' && item.decision === 'approved');
} else if (filters.status === 'rejected') {
filtered = filtered.filter(item => item.status === 'decided' && item.decision === 'rejected');
}
}
if (filters.type !== 'all') {
filtered = filtered.filter(item => item.type === filters.type);
}
if (filters.priority !== 'all') {
filtered = filtered.filter(item => item.priority === filters.priority);
}
return filtered;
}
// Расчёт KPI
function calculateKPI(data) {
const total = data.length;
const review = data.filter(item => item.status === 'review').length;
const approved = data.filter(item => item.decision === 'approved').length;
const rejected = data.filter(item => item.decision === 'rejected').length;
return { total, review, approved, rejected };
}
// Отрисовка KPI
function renderKPI(kpi) {
document.getElementById('kpi-total').textContent = kpi.total;
document.getElementById('kpi-review').textContent = kpi.review;
document.getElementById('kpi-approved').textContent = kpi.approved;
document.getElementById('kpi-rejected').textContent = kpi.rejected;
}
// Цвет статуса
function getStatusChip(status, decision) {
const chips = {
'new': '<span class="kt-ai-chip" data-tone="blue">Новый</span>',
'review': '<span class="kt-ai-chip" data-tone="orange">На рассмотрении</span>',
'decided': decision === 'approved'
? '<span class="kt-ai-chip" data-tone="green">Принято</span>'
: decision === 'rejected'
? '<span class="kt-ai-chip" data-tone="red">Отклонено</span>'
: '<span class="kt-ai-chip" data-tone="orange">На доработку</span>'
};
return chips[status] || chips['new'];
}
// Цвет приоритета
function getPriorityChip(priority) {
const chips = {
'high': '<span class="kt-ai-chip" data-tone="red">Высокий</span>',
'medium': '<span class="kt-ai-chip" data-tone="orange">Средний</span>',
'low': '<span class="kt-ai-chip" data-tone="blue">Низкий</span>'
};
return chips[priority] || chips['medium'];
}
// Отрисовка таблицы
function renderTable(data) {
const tbody = document.getElementById('requests-table-body');
const emptyState = document.getElementById('empty-state');
if (data.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = data.map(item => `
<tr>
<td>${item.date}</td>
<td>
<div style="font-weight: var(--kt-ai-weight-medium)">${item.applicant}</div>
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.department}</div>
</td>
<td>${item.typeName}</td>
<td>${getPriorityChip(item.priority)}</td>
<td>${getStatusChip(item.status, item.decision)}</td>
<td>${item.decision ? (item.decision === 'approved' ? '<span style="color: var(--kt-ai-status-ok-fg)">✓</span>' : '<span style="color: var(--kt-ai-status-risk-fg)">✗</span>') : '—'}</td>
<td class="num">
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openModal(${item.id})">Рассмотреть</button>
</td>
</tr>
`).join('');
}
// Текущие фильтры
let currentFilters = {
period: '7d',
status: 'all',
type: 'all',
priority: 'all'
};
let currentRequestId = null;
// Обновление отображения
function updateDisplay() {
const data = getDataForPeriod(currentFilters.period);
const filtered = filterData(data, currentFilters);
const kpi = calculateKPI(filtered);
renderKPI(kpi);
renderTable(filtered);
}
// Открытие модального окна
function openModal(requestId) {
const data = getDataForPeriod(currentFilters.period);
const request = data.find(item => item.id === requestId);
if (!request) return;
currentRequestId = requestId;
const details = document.getElementById('request-details');
details.innerHTML = `
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Дата</span>
<span class="kt-ai-info-value">${request.date}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Заявитель</span>
<span class="kt-ai-info-value">${request.applicant}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Отдел</span>
<span class="kt-ai-info-value">${request.department}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Тип заявки</span>
<span class="kt-ai-info-value">${request.typeName}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Приоритет</span>
<span class="kt-ai-info-value">${getPriorityChip(request.priority)}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Статус</span>
<span class="kt-ai-info-value">${getStatusChip(request.status, request.decision)}</span>
</div>
`;
document.getElementById('decision-comment').value = request.comment || '';
document.getElementById('modal-overlay').classList.add('in');
}
// Закрытие модального окна
function closeModal() {
document.getElementById('modal-overlay').classList.remove('in');
currentRequestId = null;
}
// Сохранение решения
function makeDecision(decision) {
if (!currentRequestId) return;
const comment = document.getElementById('decision-comment').value.trim();
saveDecision(currentRequestId, decision, comment);
closeModal();
updateDisplay();
// Показать уведомление
alert('Решение сохранено!');
}
// Обработчики фильтров
document.getElementById('filter-period').addEventListener('change', (e) => {
currentFilters.period = e.target.value;
updateDisplay();
});
document.getElementById('filter-status').addEventListener('change', (e) => {
currentFilters.status = e.target.value;
updateDisplay();
});
document.getElementById('filter-type').addEventListener('change', (e) => {
currentFilters.type = e.target.value;
updateDisplay();
});
document.getElementById('filter-priority').addEventListener('change', (e) => {
currentFilters.priority = e.target.value;
updateDisplay();
});
// Обработчики модального окна
document.getElementById('modal-close').addEventListener('click', closeModal);
document.getElementById('modal-overlay').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeModal();
});
document.getElementById('btn-approve').addEventListener('click', () => makeDecision('approved'));
document.getElementById('btn-reject').addEventListener('click', () => makeDecision('rejected'));
document.getElementById('btn-rework').addEventListener('click', () => makeDecision('rework'));
// Инициализация
updateDisplay();
// Сделать функцию доступной глобально
window.openModal = openModal;
</script>
</body>
</html>