Compare commits

...

1 Commits
pages ... main

View File

@ -3,41 +3,47 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Закупки — Töre</title> <title>Сравнение закупок — Töre</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css"> <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-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css"> <link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.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>"> <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> <style>
a.kt-ai-btn { text-decoration: none; } 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); } .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); } .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); } .groups-strip { display: flex; flex-direction: column; gap: var(--kt-ai-space-5); margin-top: 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); } .group-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); } .group-head { display: flex; align-items: center; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-4); padding-bottom: var(--kt-ai-space-3); border-bottom: 1px solid var(--kt-ai-border); }
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); } .group-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); }
.kpi-hint { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); } .group-meta { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.filters-bar { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-5); align-items: center; } .comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-4); }
.table-wrap { overflow-x: auto; } .compare-card { background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); position: relative; }
.kt-ai-table { width: 100%; border-collapse: collapse; } .compare-card.duplicate { border-color: var(--kt-ai-chart-red); background: rgba(255, 235, 238, 0.5); }
.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); } .compare-card.selected { border-color: var(--kt-ai-chart-blue); background: rgba(230, 244, 250, 0.5); }
.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); } .compare-card.excluded { opacity: 0.5; }
.kt-ai-table tr:hover { background: var(--kt-ai-bg-soft); } .compare-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-3); }
.num { text-align: right; font-variant-numeric: tabular-nums; } .compare-id { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.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); } .compare-name { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); }
.status-new { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); } .compare-specs { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3); }
.status-prep { background: var(--kt-ai-bg-soft); color: var(--kt-ai-chart-orange); } .spec-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--kt-ai-border); }
.status-active { background: var(--kt-ai-bg-soft); color: var(--kt-ai-chart-green); } .spec-row:last-child { border-bottom: none; }
.status-done { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); } .spec-label { 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); } .spec-value { font-weight: var(--kt-ai-weight-medium); text-align: right; }
.compare-sum { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); margin-top: var(--kt-ai-space-2); }
.compare-actions { display: flex; gap: var(--kt-ai-space-2); margin-top: var(--kt-ai-space-3); flex-wrap: wrap; }
.exclude-badge { position: absolute; top: var(--kt-ai-space-2); right: var(--kt-ai-space-2); background: var(--kt-ai-chart-red); color: white; font-size: var(--kt-ai-text-xs); padding: 2px 8px; border-radius: var(--kt-ai-radius-full); }
.decision-box { margin-top: var(--kt-ai-space-4); padding: var(--kt-ai-space-4); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); }
.decision-field { margin-bottom: var(--kt-ai-space-3); }
.decision-label { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); }
.kt-ai-textarea { width: 100%; min-height: 80px; font-family: var(--kt-ai-font-sans); }
.summary-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-6); }
.summary-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-4); text-align: center; }
.summary-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); }
.summary-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
.filters-bar { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); margin-bottom: var(--kt-ai-space-4); align-items: center; }
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); } .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> </style>
</head> </head>
<body class="kt-ai-app" data-kt-data-mode="demo"> <body class="kt-ai-app" data-kt-data-mode="demo">
@ -46,100 +52,66 @@
<div class="kt-ai-wrap kt-ai-pagehead-in"> <div class="kt-ai-wrap kt-ai-pagehead-in">
<div class="app-header"> <div class="app-header">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28"> <img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<strong>Управление закупками</strong> <strong>Сравнение закупок</strong>
</div> </div>
<span class="kt-ai-spacer"></span> <span class="kt-ai-spacer"></span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span> <span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="exportToExcel()">
📥 Excel
</button>
</div> </div>
</header> </header>
<main class="kt-ai-wrap" style="padding-top: var(--kt-ai-space-6); padding-bottom: var(--kt-ai-space-8);"> <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="filters-bar">
<div class="kt-ai-field">
<select class="kt-ai-select" id="filter-group" onchange="applyFilters()">
<option value="all">Все группы</option>
<option value="servers">Серверы</option>
<option value="cables">Кабельная продукция</option>
<option value="switches">Коммутаторы</option>
<option value="services">Услуги</option>
</select>
</div>
<div class="kt-ai-field"> <div class="kt-ai-field">
<select class="kt-ai-select" id="filter-status" onchange="applyFilters()"> <select class="kt-ai-select" id="filter-status" onchange="applyFilters()">
<option value="all">Все статусы</option> <option value="all">Все статусы</option>
<option value="new">Ещё не начаты</option> <option value="review">На рассмотрении</option>
<option value="prep">Подготовка</option> <option value="decided">Решение принято</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> </select>
</div> </div>
<span class="kt-ai-spacer"></span> <span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" onclick="resetFilters()">Сбросить</button> <span style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">
Найдено групп: <strong id="groups-count">0</strong>
</span>
</div> </div>
<!-- Таблица закупок --> <!-- Группы закупок для сравнения -->
<div class="kt-ai-card"> <div id="groups-container" class="groups-strip"></div>
<div class="kt-ai-table-wrap"> <div id="empty-state" class="empty-state" style="display: none;">
<table class="kt-ai-table"> <p>Нет групп для отображения</p>
<thead> <p style="font-size: var(--kt-ai-text-xs); margin-top: var(--kt-ai-space-2);">Попробуйте изменить параметры фильтрации</p>
<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>
<!-- График по месяцам --> <!-- Итоговая сводка -->
<div class="kt-ai-card" style="margin-top: var(--kt-ai-space-5);"> <div class="summary-strip">
<h3 style="font-size: var(--kt-ai-text-lg); margin-bottom: var(--kt-ai-space-4);">Динамика закупок</h3> <div class="summary-card">
<div class="chart-wrap"> <div class="summary-value" id="summary-total">0</div>
<div class="chart-bars" id="chart-bars"></div> <div class="summary-label">Всего строк</div>
<div class="chart-labels" id="chart-labels"></div> </div>
<div class="summary-card">
<div class="summary-value" id="summary-duplicates">0</div>
<div class="summary-label">Дубликатов</div>
</div>
<div class="summary-card">
<div class="summary-value" id="summary-excluded">0</div>
<div class="summary-label">Исключено</div>
</div>
<div class="summary-card">
<div class="summary-value" id="summary-saved">0 ₸</div>
<div class="summary-label">Экономия</div>
</div> </div>
</div> </div>
@ -148,181 +120,262 @@
<footer class="kt-ai-wrap kt-ai-pagefoot"> <footer class="kt-ai-wrap kt-ai-pagefoot">
<span>© 2026 Казахтелеком</span> <span>© 2026 Казахтелеком</span>
<span class="kt-ai-spacer"></span> <span class="kt-ai-spacer"></span>
<span style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Система управления закупками</span> <span style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">Анализ и сравнение закупок</span>
</footer> </footer>
<script> <script>
// Демо-данные с разными значениями под каждый фильтр // Демо-данные: группы похожих закупок для сравнения
const DEMO_DATA = [ const DEMO_GROUPS = [
{ 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: 'grp-001',
{ id: 'ZK-2026-003', name: 'Кабельная продукция', category: 'materials', sum: 8500000, status: 'active', date: '2025-12-20', startDate: '2026-01-05' }, name: 'Серверы',
{ id: 'ZK-2026-004', name: 'Строительство ВОЛС', category: 'construction', sum: 78000000, status: 'new', date: '2026-01-08', startDate: '' }, status: 'review',
{ id: 'ZK-2026-005', name: 'Лицензии на ПО', category: 'it', sum: 5600000, status: 'active', date: '2025-12-15', startDate: '2025-12-28' }, items: [
{ id: 'ZK-2026-006', name: 'Офисная мебель', category: 'materials', sum: 3200000, status: 'done', date: '2025-11-20', startDate: '2025-12-01' }, { id: 'ZK-2026-001', name: 'Поставка сервера Dell PowerEdge R760', specs: { 'CPU': '2x Intel Xeon Gold 6430', 'RAM': '256 GB DDR5', 'Storage': '4x 1.92TB SSD' }, sum: 12500000, isDuplicate: false, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-007', name: 'Монтаж систем безопасности', category: 'construction', sum: 15400000, status: 'prep', date: '2026-01-12', startDate: '2026-02-10' }, { id: 'ZK-2026-015', name: 'Сервер Dell PowerEdge R760', specs: { 'CPU': '2x Intel Xeon Gold 6430', 'RAM': '256 GB', 'Storage': '4x 1.92TB' }, sum: 12800000, isDuplicate: true, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-008', name: 'Обучение персонала', category: 'services', sum: 2800000, status: 'new', date: '2026-01-18', startDate: '' }, { id: 'ZK-2026-023', name: 'Серверная платформа (базовая)', specs: { 'CPU': '2x Intel Xeon Silver', 'RAM': '128 GB', 'Storage': '2x 960GB' }, sum: 8200000, isDuplicate: false, isExcluded: true, excludeReason: 'Не подходит по характеристикам' }
{ 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' }, decision: { selected: 'ZK-2026-001', comment: '' }
{ 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: '' } {
id: 'grp-002',
name: 'Кабельная продукция',
status: 'decided',
items: [
{ id: 'ZK-2026-003', name: 'Кабель оптический 48 волокон', specs: { 'Тип': 'Одномодовый', 'Длина': '10 км', 'Конструкция': 'Центральный модуль' }, sum: 4200000, isDuplicate: false, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-018', name: 'Оптический кабель 48F', specs: { 'Тип': 'Одномодовый', 'Длина': '10000 м', 'Конструкция': 'Модульная' }, sum: 4350000, isDuplicate: true, isExcluded: true, excludeReason: 'Дублирующая позиция, выбрана ZK-2026-003' }
],
decision: { selected: 'ZK-2026-003', comment: 'Выбрана более низкая цена при одинаковых характеристиках' }
},
{
id: 'grp-003',
name: 'Коммутаторы',
status: 'review',
items: [
{ id: 'ZK-2026-009', name: 'Коммутатор Huawei S5735-L24P4X', specs: { 'Порты': '24x 1GbE PoE+', 'Uplink': '4x 10G SFP+', 'L3': 'Базовая' }, sum: 3800000, isDuplicate: false, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-027', name: 'Сетевой коммутатор 24 порта', specs: { 'Порты': '24x 1GbE', 'Uplink': '4x 1G SFP', 'L3': 'Нет' }, sum: 2100000, isDuplicate: false, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-031', name: 'Huawei S5735-L24P4X-A', specs: { 'Порты': '24x PoE+', 'Uplink': '4x 10G', 'L3': 'Base' }, sum: 3950000, isDuplicate: true, isExcluded: false, excludeReason: '' }
],
decision: { selected: '', comment: '' }
},
{
id: 'grp-004',
name: 'Услуги техподдержки',
status: 'decided',
items: [
{ id: 'ZK-2026-002', name: 'Техническая поддержка ПО (1 год)', specs: { 'Срок': '12 месяцев', 'Режим': '24/7', 'СР': '4 часа' }, sum: 12000000, isDuplicate: false, isExcluded: false, excludeReason: '' },
{ id: 'ZK-2026-021', name: 'Поддержка программного обеспечения', specs: { 'Срок': '1 год', 'Режим': 'Рабочие дни', 'СР': '8 часов' }, sum: 6500000, isDuplicate: false, isExcluded: true, excludeReason: 'Не подходит режим поддержки (только рабочие дни)' }
],
decision: { selected: 'ZK-2026-002', comment: 'Требуется поддержка 24/7 по критичным системам' }
}
]; ];
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 = [ const FILTERS = [
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'chart'] }, { id: 'group', label: 'Группа', affects: ['groups'] },
{ id: 'category', label: 'Категория', affects: ['table', 'kpi'] }, { id: 'status', label: 'Статус', affects: ['groups'] }
{ id: 'period', label: 'Период', affects: ['table', 'kpi', 'chart'] }
]; ];
let currentData = [...DEMO_DATA]; let currentGroups = [...DEMO_GROUPS];
function formatSum(sum) { function formatSum(sum) {
return sum.toLocaleString('ru-KZ') + ' ₸'; return sum.toLocaleString('ru-KZ') + ' ₸';
} }
function getStatusChip(status) { function renderGroups(groups) {
const cfg = STATUS_CONFIG[status]; const container = document.getElementById('groups-container');
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'); const emptyState = document.getElementById('empty-state');
const groupsCount = document.getElementById('groups-count');
if (data.length === 0) { if (groups.length === 0) {
tbody.innerHTML = ''; container.innerHTML = '';
emptyState.style.display = 'block'; emptyState.style.display = 'block';
groupsCount.textContent = '0';
return; return;
} }
emptyState.style.display = 'none'; emptyState.style.display = 'none';
tbody.innerHTML = data.map(item => { groupsCount.textContent = groups.length;
const cfg = STATUS_CONFIG[item.status];
const btnDisabled = !cfg.canStart ? 'disabled' : ''; container.innerHTML = groups.map(group => {
const btnText = item.status === 'done' ? 'Архив' : (item.status === 'active' ? 'Управление' : 'Начать'); const itemsHtml = group.items.map((item, idx) => {
const reasonHtml = !cfg.canStart ? `<span class="block-reason">${cfg.reason}</span>` : ''; const isSelected = group.decision.selected === item.id;
const cardClass = [
'compare-card',
item.isDuplicate ? 'duplicate' : '',
isSelected ? 'selected' : '',
item.isExcluded ? 'excluded' : ''
].filter(Boolean).join(' ');
const specsHtml = Object.entries(item.specs).map(([label, value]) =>
`<div class="spec-row"><span class="spec-label">${label}</span><span class="spec-value">${value}</span></div>`
).join('');
const excludeBadge = item.isExcluded ? `<span class="exclude-badge">Исключена</span>` : '';
const dupBadge = item.isDuplicate && !item.isExcluded ? `<span class="exclude-badge" style="background:var(--kt-ai-chart-orange)">Дубль</span>` : '';
const actionButtons = !item.isExcluded ? `
<button class="kt-ai-btn" data-size="sm" ${isSelected ? 'data-variant="primary"' : ''} onclick="selectItem('${group.id}', '${item.id}')">
${isSelected ? '✓ Выбрана' : 'Выбрать'}
</button>
<button class="kt-ai-btn" data-size="sm" data-variant="ghost" onclick="excludeItem('${group.id}', '${item.id}')">
Исключить
</button>
` : `<span style="font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted)">Исключена: ${item.excludeReason}</span>`;
return `
<div class="${cardClass}">
${excludeBadge}${dupBadge}
<div class="compare-header">
<span class="compare-id">${item.id}</span>
</div>
<div class="compare-name">${item.name}</div>
<div class="compare-specs">${specsHtml}</div>
<div class="compare-sum">${formatSum(item.sum)}</div>
<div class="compare-actions">${actionButtons}</div>
</div>
`;
}).join('');
return `<tr> const hasDecision = group.decision.selected || group.items.some(i => i.isExcluded);
<td style="font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs);">${item.id}</td> const statusChip = group.status === 'decided'
<td>${item.name}</td> ? '<span class="kt-ai-chip" data-tone="green">Решение принято</span>'
<td>${CATEGORY_NAMES[item.category]}</td> : '<span class="kt-ai-chip" data-tone="orange">На рассмотрении</span>';
<td class="num">${formatSum(item.sum)}</td>
<td>${getStatusChip(item.status)}</td> const decisionComment = group.decision.comment
<td>${item.startDate || '—'}</td> ? `<p style="font-size:var(--kt-ai-text-sm);margin-top:var(--kt-ai-space-2);color:var(--kt-ai-fg-muted)">${group.decision.comment}</p>`
<td> : '';
<button class="kt-ai-btn" data-size="sm" ${btnDisabled} onclick="handleAction('${item.id}')">${btnText}</button>
${reasonHtml} return `
</td> <div class="group-card">
</tr>`; <div class="group-head">
}).join(''); <span class="group-title">${group.name}</span>
} ${statusChip}
<span class="kt-ai-spacer"></span>
function renderKPI(data) { <span class="group-meta">${group.items.length} строк</span>
const total = data.length; </div>
const notStarted = data.filter(i => i.status === 'new').length; <div class="comparison-grid">${itemsHtml}</div>
const active = data.filter(i => i.status === 'active').length; <div class="decision-box">
const sum = data.reduce((acc, i) => acc + i.sum, 0); <div class="decision-label">Комментарий к решению:</div>
<textarea class="kt-ai-textarea" placeholder="Введите обоснование выбора..." onchange="updateComment('${group.id}', this.value)">${group.decision.comment}</textarea>
document.getElementById('kpi-total').textContent = total; ${decisionComment}
document.getElementById('kpi-not-started').textContent = notStarted; </div>
document.getElementById('kpi-active').textContent = active; </div>
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(''); }).join('');
labelsContainer.innerHTML = months.map(m => `<span>${m}</span>`).join(''); updateSummary(groups);
}
function updateSummary(groups) {
let total = 0, duplicates = 0, excluded = 0, saved = 0;
groups.forEach(g => {
g.items.forEach(i => {
total++;
if (i.isDuplicate) duplicates++;
if (i.isExcluded) excluded++;
});
// Экономия: разница между самой дорогой и выбранной
const activeItems = g.items.filter(i => !i.isExcluded);
if (activeItems.length > 1) {
const maxSum = Math.max(...activeItems.map(i => i.sum));
const selected = g.items.find(i => i.id === g.decision.selected);
if (selected) {
saved += maxSum - selected.sum;
}
}
});
document.getElementById('summary-total').textContent = total;
document.getElementById('summary-duplicates').textContent = duplicates;
document.getElementById('summary-excluded').textContent = excluded;
document.getElementById('summary-saved').textContent = formatSum(saved);
}
function selectItem(groupId, itemId) {
const group = currentGroups.find(g => g.id === groupId);
if (!group) return;
if (group.decision.selected === itemId) {
group.decision.selected = '';
} else {
group.decision.selected = itemId;
group.status = 'decided';
}
renderGroups(currentGroups);
}
function excludeItem(groupId, itemId) {
const group = currentGroups.find(g => g.id === groupId);
if (!group) return;
const item = group.items.find(i => i.id === itemId);
if (!item) return;
const reason = prompt('Причина исключения:', 'Дублирующая позиция');
if (reason === null) return;
item.isExcluded = true;
item.excludeReason = reason || 'Исключено без указания причины';
if (group.decision.selected === itemId) {
group.decision.selected = '';
group.status = 'review';
}
renderGroups(currentGroups);
}
function updateComment(groupId, comment) {
const group = currentGroups.find(g => g.id === groupId);
if (!group) return;
group.decision.comment = comment;
} }
function applyFilters() { function applyFilters() {
const groupFilter = document.getElementById('filter-group').value;
const statusFilter = document.getElementById('filter-status').value; 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]; let filtered = [...DEMO_GROUPS];
if (groupFilter !== 'all') {
filtered = filtered.filter(g => g.id === groupFilter || g.name.toLowerCase().includes(groupFilter));
}
if (statusFilter !== 'all') { if (statusFilter !== 'all') {
filtered = filtered.filter(i => i.status === statusFilter); filtered = filtered.filter(g => g.status === statusFilter);
} }
if (categoryFilter !== 'all') { currentGroups = filtered;
filtered = filtered.filter(i => i.category === categoryFilter); renderGroups(filtered);
} }
function exportToExcel() {
let csv = '\uFEFF';
csv += 'Группа;ID;Наименование;Характеристики;Сумма;Статус;Выбрана;Исключена;Причина\n';
if (periodFilter !== 'all') { currentGroups.forEach(g => {
const now = new Date('2026-01-20'); g.items.forEach(i => {
const monthAgo = new Date('2026-01-01'); const specs = Object.entries(i.specs).map(([k,v]) => `${k}: ${v}`).join('; ');
const quarterAgo = new Date('2025-11-01'); const isSelected = i.id === g.decision.selected ? 'Да' : 'Нет';
const isExcluded = i.isExcluded ? 'Да' : 'Нет';
filtered = filtered.filter(i => { csv += `${g.name};${i.id};"${i.name}";"${specs}";${i.sum};${g.status};${isSelected};${isExcluded};"${i.excludeReason}"\n`;
const d = new Date(i.date);
if (periodFilter === 'month') return d >= monthAgo;
if (periodFilter === 'quarter') return d >= quarterAgo;
return true;
}); });
} });
currentData = filtered; const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
renderTable(filtered); const url = URL.createObjectURL(blob);
renderKPI(filtered); const a = document.createElement('a');
} a.href = url;
a.download = 'sravnenie_zakupok.csv';
function resetFilters() { a.click();
document.getElementById('filter-status').value = 'all'; URL.revokeObjectURL(url);
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); renderGroups(DEMO_GROUPS);
renderKPI(DEMO_DATA);
renderChart(DEMO_DATA);
</script> </script>
</body> </body>