330 lines
16 KiB
HTML
330 lines
16 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>Закупки — Töre</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; }
|
||
.kt-ai-app { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
|
||
.app-header { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
|
||
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.kpi-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); }
|
||
.kpi-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
|
||
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
|
||
.kpi-hint { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
|
||
.filters-bar { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-5); align-items: center; }
|
||
.table-wrap { overflow-x: auto; }
|
||
.kt-ai-table { width: 100%; border-collapse: collapse; }
|
||
.kt-ai-table th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); border-bottom: 1px solid var(--kt-ai-border); }
|
||
.kt-ai-table td { padding: var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); font-size: var(--kt-ai-text-sm); }
|
||
.kt-ai-table tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.status-chip { display: inline-flex; padding: 2px 8px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
|
||
.status-new { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); }
|
||
.status-prep { background: var(--kt-ai-bg-soft); color: var(--kt-ai-chart-orange); }
|
||
.status-active { background: var(--kt-ai-bg-soft); color: var(--kt-ai-chart-green); }
|
||
.status-done { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); }
|
||
.block-reason { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-left: var(--kt-ai-space-2); }
|
||
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
|
||
.chart-wrap { height: 200px; margin-top: var(--kt-ai-space-6); }
|
||
.chart-bars { display: flex; align-items: flex-end; gap: 8px; height: 160px; padding-top: var(--kt-ai-space-4); }
|
||
.chart-bar { flex: 1; background: var(--kt-ai-chart-blue); border-radius: 2px 2px 0 0; min-width: 24px; position: relative; }
|
||
.chart-bar[data-kt-tip]::after { content: attr(data-kt-tip); position: absolute; top: -24px; left: 50%; transform: translateX(-50%); font-size: var(--kt-ai-text-xs); background: var(--kt-ai-fg); color: var(--kt-ai-bg); padding: 2px 6px; border-radius: 4px; opacity: 0; pointer-events: none; transition: opacity 0.2s; }
|
||
.chart-bar:hover[data-kt-tip]::after { opacity: 1; }
|
||
.chart-labels { display: flex; justify-content: space-between; margin-top: var(--kt-ai-space-2); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
|
||
</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="Казахтелеком" 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" style="padding-top: var(--kt-ai-space-6); padding-bottom: var(--kt-ai-space-8);">
|
||
|
||
<!-- KPI панель -->
|
||
<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-not-started">0</div>
|
||
<div class="kpi-label">Ещё не начаты</div>
|
||
<div class="kpi-hint">Требуют подготовки</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-active">0</div>
|
||
<div class="kpi-label">В работе</div>
|
||
<div class="kpi-hint">Приём заявок идёт</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpi-sum">0 ₸</div>
|
||
<div class="kpi-label">Общая сумма</div>
|
||
<div class="kpi-hint">Плановый бюджет</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Фильтры -->
|
||
<div class="filters-bar">
|
||
<div class="kt-ai-field">
|
||
<select class="kt-ai-select" id="filter-status" onchange="applyFilters()">
|
||
<option value="all">Все статусы</option>
|
||
<option value="new">Ещё не начаты</option>
|
||
<option value="prep">Подготовка</option>
|
||
<option value="active">В работе</option>
|
||
<option value="done">Завершены</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<select class="kt-ai-select" id="filter-category" onchange="applyFilters()">
|
||
<option value="all">Все категории</option>
|
||
<option value="it">ИТ и оборудование</option>
|
||
<option value="services">Услуги</option>
|
||
<option value="materials">Материалы</option>
|
||
<option value="construction">Строительство</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-field">
|
||
<select class="kt-ai-select" id="filter-period" onchange="applyFilters()">
|
||
<option value="all">Любой период</option>
|
||
<option value="month">Этот месяц</option>
|
||
<option value="quarter">Квартал</option>
|
||
</select>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="resetFilters()">Сбросить</button>
|
||
</div>
|
||
|
||
<!-- Таблица закупок -->
|
||
<div class="kt-ai-card">
|
||
<div 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="procurements-table"></tbody>
|
||
</table>
|
||
</div>
|
||
<div id="empty-state" class="empty-state" style="display: none;">
|
||
<p>Нет данных по выбранным фильтрам</p>
|
||
<p style="font-size: var(--kt-ai-text-xs); margin-top: var(--kt-ai-space-2);">Попробуйте изменить параметры фильтрации</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- График по месяцам -->
|
||
<div class="kt-ai-card" style="margin-top: var(--kt-ai-space-5);">
|
||
<h3 style="font-size: var(--kt-ai-text-lg); margin-bottom: var(--kt-ai-space-4);">Динамика закупок</h3>
|
||
<div class="chart-wrap">
|
||
<div class="chart-bars" id="chart-bars"></div>
|
||
<div class="chart-labels" id="chart-labels"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||
<span>© 2026 Казахтелеком</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Система управления закупками</span>
|
||
</footer>
|
||
|
||
<script>
|
||
// Демо-данные с разными значениями под каждый фильтр
|
||
const DEMO_DATA = [
|
||
{ id: 'ZK-2026-001', name: 'Поставка серверного оборудования', category: 'it', sum: 45000000, status: 'new', date: '2026-01-15', startDate: '' },
|
||
{ id: 'ZK-2026-002', name: 'Услуги техподдержки ПО', category: 'services', sum: 12000000, status: 'prep', date: '2026-01-10', startDate: '2026-02-01' },
|
||
{ id: 'ZK-2026-003', name: 'Кабельная продукция', category: 'materials', sum: 8500000, status: 'active', date: '2025-12-20', startDate: '2026-01-05' },
|
||
{ id: 'ZK-2026-004', name: 'Строительство ВОЛС', category: 'construction', sum: 78000000, status: 'new', date: '2026-01-08', startDate: '' },
|
||
{ id: 'ZK-2026-005', name: 'Лицензии на ПО', category: 'it', sum: 5600000, status: 'active', date: '2025-12-15', startDate: '2025-12-28' },
|
||
{ id: 'ZK-2026-006', name: 'Офисная мебель', category: 'materials', sum: 3200000, status: 'done', date: '2025-11-20', startDate: '2025-12-01' },
|
||
{ id: 'ZK-2026-007', name: 'Монтаж систем безопасности', category: 'construction', sum: 15400000, status: 'prep', date: '2026-01-12', startDate: '2026-02-10' },
|
||
{ id: 'ZK-2026-008', name: 'Обучение персонала', category: 'services', sum: 2800000, status: 'new', date: '2026-01-18', startDate: '' },
|
||
{ id: 'ZK-2026-009', name: 'Сетевые коммутаторы', category: 'it', sum: 18900000, status: 'active', date: '2025-12-28', startDate: '2026-01-10' },
|
||
{ id: 'ZK-2026-010', name: 'Уборка помещений', category: 'services', sum: 4100000, status: 'done', date: '2025-10-15', startDate: '2025-11-01' },
|
||
{ id: 'ZK-2026-011', name: 'Оптические патч-корды', category: 'materials', sum: 1850000, status: 'prep', date: '2026-01-05', startDate: '2026-01-25' },
|
||
{ id: 'ZK-2026-012', name: 'Проектирование ЦОД', category: 'construction', sum: 32000000, status: 'new', date: '2026-01-20', startDate: '' }
|
||
];
|
||
|
||
const CATEGORY_NAMES = {
|
||
'it': 'ИТ и оборудование',
|
||
'services': 'Услуги',
|
||
'materials': 'Материалы',
|
||
'construction': 'Строительство'
|
||
};
|
||
|
||
const STATUS_CONFIG = {
|
||
'new': { label: 'Ещё не начата', class: 'status-new', canStart: false, reason: 'Срок не заполнен — укажите дату начала' },
|
||
'prep': { label: 'Подготовка', class: 'status-prep', canStart: false, reason: 'Документы на согласовании' },
|
||
'active': { label: 'В работе', class: 'status-active', canStart: true, reason: '' },
|
||
'done': { label: 'Завершена', class: 'status-done', canStart: false, reason: 'Процедура завершена' }
|
||
};
|
||
|
||
const FILTERS = [
|
||
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'chart'] },
|
||
{ id: 'category', label: 'Категория', affects: ['table', 'kpi'] },
|
||
{ id: 'period', label: 'Период', affects: ['table', 'kpi', 'chart'] }
|
||
];
|
||
|
||
let currentData = [...DEMO_DATA];
|
||
|
||
function formatSum(sum) {
|
||
return sum.toLocaleString('ru-KZ') + ' ₸';
|
||
}
|
||
|
||
function getStatusChip(status) {
|
||
const cfg = STATUS_CONFIG[status];
|
||
return `<span class="status-chip ${cfg.class}">${cfg.label}</span>`;
|
||
}
|
||
|
||
function renderTable(data) {
|
||
const tbody = document.getElementById('procurements-table');
|
||
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 => {
|
||
const cfg = STATUS_CONFIG[item.status];
|
||
const btnDisabled = !cfg.canStart ? 'disabled' : '';
|
||
const btnText = item.status === 'done' ? 'Архив' : (item.status === 'active' ? 'Управление' : 'Начать');
|
||
const reasonHtml = !cfg.canStart ? `<span class="block-reason">${cfg.reason}</span>` : '';
|
||
|
||
return `<tr>
|
||
<td style="font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs);">${item.id}</td>
|
||
<td>${item.name}</td>
|
||
<td>${CATEGORY_NAMES[item.category]}</td>
|
||
<td class="num">${formatSum(item.sum)}</td>
|
||
<td>${getStatusChip(item.status)}</td>
|
||
<td>${item.startDate || '—'}</td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-size="sm" ${btnDisabled} onclick="handleAction('${item.id}')">${btnText}</button>
|
||
${reasonHtml}
|
||
</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderKPI(data) {
|
||
const total = data.length;
|
||
const notStarted = data.filter(i => i.status === 'new').length;
|
||
const active = data.filter(i => i.status === 'active').length;
|
||
const sum = data.reduce((acc, i) => acc + i.sum, 0);
|
||
|
||
document.getElementById('kpi-total').textContent = total;
|
||
document.getElementById('kpi-not-started').textContent = notStarted;
|
||
document.getElementById('kpi-active').textContent = active;
|
||
document.getElementById('kpi-sum').textContent = formatSum(sum);
|
||
}
|
||
|
||
function renderChart(data) {
|
||
const months = ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн'];
|
||
const monthData = [3, 5, 2, 4, 1, 2];
|
||
const max = Math.max(...monthData);
|
||
|
||
const barsContainer = document.getElementById('chart-bars');
|
||
const labelsContainer = document.getElementById('chart-labels');
|
||
|
||
barsContainer.innerHTML = monthData.map((val, idx) => {
|
||
const height = (val / max) * 140;
|
||
return `<div class="chart-bar" style="height: ${height}px;" data-kt-tip="${val} закуп.${idx === 0 ? ' (демо)' : ''}"></div>`;
|
||
}).join('');
|
||
|
||
labelsContainer.innerHTML = months.map(m => `<span>${m}</span>`).join('');
|
||
}
|
||
|
||
function applyFilters() {
|
||
const statusFilter = document.getElementById('filter-status').value;
|
||
const categoryFilter = document.getElementById('filter-category').value;
|
||
const periodFilter = document.getElementById('filter-period').value;
|
||
|
||
let filtered = [...DEMO_DATA];
|
||
|
||
if (statusFilter !== 'all') {
|
||
filtered = filtered.filter(i => i.status === statusFilter);
|
||
}
|
||
|
||
if (categoryFilter !== 'all') {
|
||
filtered = filtered.filter(i => i.category === categoryFilter);
|
||
}
|
||
|
||
if (periodFilter !== 'all') {
|
||
const now = new Date('2026-01-20');
|
||
const monthAgo = new Date('2026-01-01');
|
||
const quarterAgo = new Date('2025-11-01');
|
||
|
||
filtered = filtered.filter(i => {
|
||
const d = new Date(i.date);
|
||
if (periodFilter === 'month') return d >= monthAgo;
|
||
if (periodFilter === 'quarter') return d >= quarterAgo;
|
||
return true;
|
||
});
|
||
}
|
||
|
||
currentData = filtered;
|
||
renderTable(filtered);
|
||
renderKPI(filtered);
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('filter-status').value = 'all';
|
||
document.getElementById('filter-category').value = 'all';
|
||
document.getElementById('filter-period').value = 'all';
|
||
applyFilters();
|
||
}
|
||
|
||
function handleAction(id) {
|
||
const item = DEMO_DATA.find(i => i.id === id);
|
||
if (!item) return;
|
||
|
||
if (item.status === 'new') {
|
||
alert('Нельзя начать: ' + STATUS_CONFIG['new'].reason);
|
||
return;
|
||
}
|
||
if (item.status === 'prep') {
|
||
alert('Нельзя начать: ' + STATUS_CONFIG['prep'].reason);
|
||
return;
|
||
}
|
||
if (item.status === 'done') {
|
||
alert('Процедура уже завершена');
|
||
return;
|
||
}
|
||
|
||
alert('Открыто управление закупкой ' + item.id + ': ' + item.name);
|
||
}
|
||
|
||
// Инициализация
|
||
renderTable(DEMO_DATA);
|
||
renderKPI(DEMO_DATA);
|
||
renderChart(DEMO_DATA);
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|