661 lines
32 KiB
HTML
661 lines
32 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>
|
||
.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-table .group-row { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); cursor: pointer; }
|
||
.kt-ai-table .group-row:hover { background: var(--kt-ai-border); }
|
||
.kt-ai-table .group-icon { display: inline-block; width: 20px; text-align: center; margin-right: var(--kt-ai-space-2); transition: transform 0.2s; }
|
||
.kt-ai-table .group-row.collapsed .group-icon { transform: rotate(-90deg); }
|
||
.kt-ai-table .group-row.collapsed + tr { display: none; }
|
||
.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: 640px; 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-compare-block { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); margin: var(--kt-ai-space-3) 0; }
|
||
.kt-ai-compare-title { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); }
|
||
.kt-ai-compare-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--kt-ai-space-3); }
|
||
.kt-ai-compare-item { font-size: var(--kt-ai-text-xs); }
|
||
.kt-ai-compare-item .label { color: var(--kt-ai-fg-faint); margin-bottom: var(--kt-ai-space-1); }
|
||
.kt-ai-compare-item .value { 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); }
|
||
.kt-ai-actions-bar { display: flex; justify-content: space-between; align-items: center; margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-4); border-top: 1px solid var(--kt-ai-border); }
|
||
.kt-ai-bulk-actions { display: flex; gap: var(--kt-ai-space-3); }
|
||
@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%; }
|
||
.kt-ai-compare-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</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-excluded" 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-group" style="min-width: 160px">
|
||
<option value="all">Все группы</option>
|
||
<option value="it">ИТ-оборудование</option>
|
||
<option value="network">Сетевое оборудование</option>
|
||
<option value="software">Программное обеспечение</option>
|
||
<option value="services">Услуги</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="review">На согласовании</option>
|
||
<option value="approved">Согласовано</option>
|
||
<option value="rejected">Отклонено</option>
|
||
<option value="excluded">Исключено</option>
|
||
</select>
|
||
</div>
|
||
<div class="kt-ai-filter-group">
|
||
<label class="kt-ai-filter-label">Город</label>
|
||
<select class="kt-ai-select" id="filter-city" style="min-width: 140px">
|
||
<option value="all">Все города</option>
|
||
<option value="almaty">Алматы</option>
|
||
<option value="astana">Астана</option>
|
||
<option value="shymkent">Шымкент</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-compat" style="min-width: 160px">
|
||
<option value="all">Все</option>
|
||
<option value="compatible">Совместима</option>
|
||
<option value="check">Требует проверки</option>
|
||
<option value="incompatible">Несовместима</option>
|
||
</select>
|
||
</div>
|
||
<span class="kt-ai-spacer"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" id="btn-export" onclick="exportToExcel()">📄 Выгрузить Excel</button>
|
||
</section>
|
||
|
||
<section class="kt-ai-table-wrap">
|
||
<table class="kt-ai-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 40px"></th>
|
||
<th>Наименование</th>
|
||
<th>Характеристики</th>
|
||
<th>Срок</th>
|
||
<th>Город</th>
|
||
<th class="num">Цена (₸)</th>
|
||
<th>Совместимость</th>
|
||
<th>Статус</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="positions-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>
|
||
|
||
<div class="kt-ai-actions-bar">
|
||
<div class="kt-ai-bulk-actions">
|
||
<button class="kt-ai-btn" data-variant="primary" id="btn-approve-all" onclick="approveAll()">✓ Согласовать все совместимые</button>
|
||
<button class="kt-ai-btn" data-variant="danger" id="btn-exclude-incompatible" onclick="excludeIncompatible()">✗ Исключить несовместимые</button>
|
||
</div>
|
||
<p class="kt-ai-decision-note" id="bulk-result"></p>
|
||
</div>
|
||
|
||
</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">×</button>
|
||
</div>
|
||
<div class="kt-ai-modal-body">
|
||
<div id="position-details"></div>
|
||
|
||
<div class="kt-ai-compare-block">
|
||
<div class="kt-ai-compare-title">📊 Сравнение характеристик</div>
|
||
<div class="kt-ai-compare-grid" id="compare-grid"></div>
|
||
</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-exclude">Исключить</button>
|
||
</div>
|
||
<p class="kt-ai-decision-note">Решение будет сохранено в localStorage</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Реестр фильтров
|
||
const FILTERS = [
|
||
{ id: 'group', label: 'Группа', affects: ['table', 'kpi', 'empty'] },
|
||
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'empty'] },
|
||
{ id: 'city', label: 'Город', affects: ['table', 'kpi', 'empty'] },
|
||
{ id: 'compat', label: 'Совместимость', affects: ['table', 'kpi', 'empty'] }
|
||
];
|
||
|
||
// Демо-данные по группам
|
||
const DEMO_DATA = {
|
||
'it': [
|
||
{ id: 1, group: 'it', groupName: 'ИТ-оборудование', name: 'Сервер Dell PowerEdge R750', specs: '2xCPU 16-core, 128GB RAM, 4TB SSD', deadline: '2026-12-15', city: 'almaty', cityName: 'Алматы', price: 4500000, qty: 2, supplier: 'Dell Technologies', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 2, group: 'it', groupName: 'ИТ-оборудование', name: 'Рабочая станция HP Z4 G5', specs: 'CPU i9-13900K, 64GB RAM, 2TB NVMe', deadline: '2026-11-30', city: 'astana', cityName: 'Астана', price: 1200000, qty: 5, supplier: 'HP Inc.', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 3, group: 'it', groupName: 'ИТ-оборудование', name: 'Ноутбук Lenovo ThinkPad P16', specs: 'CPU i7-13850HX, 32GB RAM, 1TB SSD', deadline: '2026-12-01', city: 'shymkent', cityName: 'Шымкент', price: 850000, qty: 10, supplier: 'Lenovo', compat: 'check', compatName: 'Требует проверки', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
|
||
],
|
||
'network': [
|
||
{ id: 4, group: 'network', groupName: 'Сетевое оборудование', name: 'Маршрутизатор Cisco ISR 4351', specs: '3xGE WAN, 8xGE LAN, IOS XE', deadline: '2027-01-15', city: 'almaty', cityName: 'Алматы', price: 2800000, qty: 3, supplier: 'Cisco', compat: 'compatible', compatName: 'Совместима', status: 'approved', statusName: 'Согласовано', decision: 'approved', comment: 'Совместимость подтверждена' },
|
||
{ id: 5, group: 'network', groupName: 'Сетевое оборудование', name: 'Коммутатор Huawei S5735-L48', specs: '48xGE, 4x10G SFP+, PoE+', deadline: '2026-12-20', city: 'astana', cityName: 'Астана', price: 650000, qty: 8, supplier: 'Huawei', compat: 'incompatible', compatName: 'Несовместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 6, group: 'network', groupName: 'Сетевое оборудование', name: 'СКС-комплект 19"', specs: 'Шкаф 42U, патч-панели, кабель', deadline: '2026-11-25', city: 'other', cityName: 'Караганда', price: 380000, qty: 4, supplier: 'Legrand', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
|
||
],
|
||
'software': [
|
||
{ id: 7, group: 'software', groupName: 'Программное обеспечение', name: 'Лицензия Windows Server 2022', specs: 'Datacenter, 16 core, CAL', deadline: '2026-12-10', city: 'almaty', cityName: 'Алматы', price: 450000, qty: 5, supplier: 'Microsoft', compat: 'compatible', compatName: 'Совместима', status: 'approved', statusName: 'Согласовано', decision: 'approved', comment: 'Стандартная лицензия' },
|
||
{ id: 8, group: 'software', groupName: 'Программное обеспечение', name: 'Антивирус Kaspersky Endpoint', specs: '3 года, 500 устройств', deadline: '2027-01-05', city: 'astana', cityName: 'Астана', price: 1200000, qty: 1, supplier: 'Kaspersky', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 9, group: 'software', groupName: 'Программное обеспечение', name: '1С:Предприятие 8.3', specs: 'Сервер + 50 клиентских лицензий', deadline: '2026-12-05', city: 'shymkent', cityName: 'Шымкент', price: 980000, qty: 1, supplier: '1С', compat: 'check', compatName: 'Требует проверки', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
|
||
],
|
||
'services': [
|
||
{ id: 10, group: 'services', groupName: 'Услуги', name: 'Техническая поддержка 24/7', specs: '3 года, SLA 99.9%', deadline: '2027-02-01', city: 'almaty', cityName: 'Алматы', price: 5400000, qty: 1, supplier: 'КТ-Сервис', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 11, group: 'services', groupName: 'Услуги', name: 'Обучение персонала', specs: 'Курсы Cisco CCNA, 20 человек', deadline: '2027-03-15', city: 'astana', cityName: 'Астана', price: 2000000, qty: 1, supplier: 'УЦ "Сетевые Технологии"', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
|
||
{ id: 12, group: 'services', groupName: 'Услуги', name: 'Аутсорсинг ИТ-инфраструктуры', specs: '2 года, полный спектр услуг', deadline: '2027-01-20', city: 'other', cityName: 'Актобе', price: 8600000, qty: 1, supplier: 'КТ-Аутсорсинг', compat: 'incompatible', compatName: 'Несовместима', status: 'excluded', statusName: 'Исключено', decision: 'excluded', comment: 'Дублирует внутренние ресурсы' }
|
||
]
|
||
};
|
||
|
||
// Пустой срез для демонстрации
|
||
DEMO_DATA['other'] = [];
|
||
|
||
// Загрузка сохранённых решений из localStorage
|
||
function loadSavedDecisions() {
|
||
const saved = localStorage.getItem('procurementDecisions');
|
||
return saved ? JSON.parse(saved) : {};
|
||
}
|
||
|
||
// Сохранение решения в localStorage
|
||
function saveDecision(positionId, decision, comment) {
|
||
const decisions = loadSavedDecisions();
|
||
decisions[positionId] = { decision, comment, date: new Date().toISOString() };
|
||
localStorage.setItem('procurementDecisions', JSON.stringify(decisions));
|
||
}
|
||
|
||
// Применение сохранённых решений к демо-данным
|
||
function applySavedDecisions(data) {
|
||
const decisions = loadSavedDecisions();
|
||
return data.map(item => {
|
||
if (decisions[item.id]) {
|
||
const saved = decisions[item.id];
|
||
const statusMap = {
|
||
'approved': 'approved',
|
||
'rejected': 'rejected',
|
||
'excluded': 'excluded'
|
||
};
|
||
const statusNameMap = {
|
||
'approved': 'Согласовано',
|
||
'rejected': 'Отклонено',
|
||
'excluded': 'Исключено'
|
||
};
|
||
return {
|
||
...item,
|
||
decision: saved.decision,
|
||
status: statusMap[saved.decision] || item.status,
|
||
statusName: statusNameMap[saved.decision] || item.statusName,
|
||
comment: saved.comment
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
}
|
||
|
||
// Получение всех данных
|
||
function getAllData() {
|
||
let allData = [];
|
||
for (const group in DEMO_DATA) {
|
||
allData = allData.concat(DEMO_DATA[group]);
|
||
}
|
||
return applySavedDecisions(allData);
|
||
}
|
||
|
||
// Фильтрация данных
|
||
function filterData(data, filters) {
|
||
let filtered = [...data];
|
||
|
||
if (filters.group !== 'all') {
|
||
filtered = filtered.filter(item => item.group === filters.group);
|
||
}
|
||
|
||
if (filters.status !== 'all') {
|
||
if (filters.status === 'review') {
|
||
filtered = filtered.filter(item => item.status === 'review');
|
||
} else if (filters.status === 'approved') {
|
||
filtered = filtered.filter(item => item.status === 'approved');
|
||
} else if (filters.status === 'rejected') {
|
||
filtered = filtered.filter(item => item.status === 'rejected');
|
||
} else if (filters.status === 'excluded') {
|
||
filtered = filtered.filter(item => item.status === 'excluded');
|
||
}
|
||
}
|
||
|
||
if (filters.city !== 'all') {
|
||
filtered = filtered.filter(item => item.city === filters.city);
|
||
}
|
||
|
||
if (filters.compat !== 'all') {
|
||
filtered = filtered.filter(item => item.compat === filters.compat);
|
||
}
|
||
|
||
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.status === 'approved').length;
|
||
const excluded = data.filter(item => item.status === 'excluded').length;
|
||
return { total, review, approved, excluded };
|
||
}
|
||
|
||
// Отрисовка 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-excluded').textContent = kpi.excluded;
|
||
}
|
||
|
||
// Цвет статуса
|
||
function getStatusChip(status) {
|
||
const chips = {
|
||
'review': '<span class="kt-ai-chip" data-tone="orange">На согласовании</span>',
|
||
'approved': '<span class="kt-ai-chip" data-tone="green">Согласовано</span>',
|
||
'rejected': '<span class="kt-ai-chip" data-tone="red">Отклонено</span>',
|
||
'excluded': '<span class="kt-ai-chip" data-tone="red">Исключено</span>'
|
||
};
|
||
return chips[status] || chips['review'];
|
||
}
|
||
|
||
// Цвет совместимости
|
||
function getCompatChip(compat) {
|
||
const chips = {
|
||
'compatible': '<span class="kt-ai-chip" data-tone="green">Совместима</span>',
|
||
'check': '<span class="kt-ai-chip" data-tone="orange">Требует проверки</span>',
|
||
'incompatible': '<span class="kt-ai-chip" data-tone="red">Несовместима</span>'
|
||
};
|
||
return chips[compat] || chips['check'];
|
||
}
|
||
|
||
// Форматирование цены
|
||
function formatPrice(price) {
|
||
return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||
}
|
||
|
||
// Группировка данных по категориям
|
||
function groupByCategory(data) {
|
||
const groups = {};
|
||
data.forEach(item => {
|
||
if (!groups[item.group]) {
|
||
groups[item.group] = [];
|
||
}
|
||
groups[item.group].push(item);
|
||
});
|
||
return groups;
|
||
}
|
||
|
||
// Отрисовка таблицы с группировкой
|
||
function renderTable(data) {
|
||
const tbody = document.getElementById('positions-table-body');
|
||
const emptyState = document.getElementById('empty-state');
|
||
|
||
if (data.length === 0) {
|
||
tbody.innerHTML = '';
|
||
emptyState.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyState.style.display = 'none';
|
||
const grouped = groupByCategory(data);
|
||
|
||
let html = '';
|
||
for (const groupKey in grouped) {
|
||
const items = grouped[groupKey];
|
||
if (items.length === 0) continue;
|
||
|
||
const groupName = items[0].groupName;
|
||
html += `
|
||
<tr class="group-row" onclick="toggleGroup('${groupKey}')">
|
||
<td><span class="group-icon">▼</span></td>
|
||
<td colspan="8">${groupName} (${items.length} поз.)</td>
|
||
</tr>
|
||
`;
|
||
|
||
items.forEach(item => {
|
||
html += `
|
||
<tr class="group-item ${groupKey}">
|
||
<td></td>
|
||
<td>
|
||
<div style="font-weight: var(--kt-ai-weight-medium)">${item.name}</div>
|
||
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.supplier}</div>
|
||
</td>
|
||
<td style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.specs}</td>
|
||
<td>${item.deadline}</td>
|
||
<td>${item.cityName}</td>
|
||
<td class="num">${formatPrice(item.price)} ₸</td>
|
||
<td>${getCompatChip(item.compat)}</td>
|
||
<td>${getStatusChip(item.status)}</td>
|
||
<td class="num">
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openModal(${item.id})">Согласовать</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
}
|
||
|
||
tbody.innerHTML = html;
|
||
}
|
||
|
||
// Свёртка/развёртка группы
|
||
function toggleGroup(groupKey) {
|
||
const rows = document.querySelectorAll(`.group-item.${groupKey}`);
|
||
const groupRow = document.querySelector(`tr[onclick="toggleGroup('${groupKey}')"]`);
|
||
|
||
rows.forEach(row => {
|
||
row.style.display = row.style.display === 'none' ? '' : 'none';
|
||
});
|
||
|
||
groupRow.classList.toggle('collapsed');
|
||
}
|
||
|
||
// Текущие фильтры
|
||
let currentFilters = {
|
||
group: 'all',
|
||
status: 'all',
|
||
city: 'all',
|
||
compat: 'all'
|
||
};
|
||
|
||
let currentPositionId = null;
|
||
|
||
// Обновление отображения
|
||
function updateDisplay() {
|
||
const data = getAllData();
|
||
const filtered = filterData(data, currentFilters);
|
||
const kpi = calculateKPI(filtered);
|
||
|
||
renderKPI(kpi);
|
||
renderTable(filtered);
|
||
}
|
||
|
||
// Открытие модального окна
|
||
function openModal(positionId) {
|
||
const data = getAllData();
|
||
const position = data.find(item => item.id === positionId);
|
||
|
||
if (!position) return;
|
||
|
||
currentPositionId = positionId;
|
||
|
||
const details = document.getElementById('position-details');
|
||
details.innerHTML = `
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Наименование</span>
|
||
<span class="kt-ai-info-value">${position.name}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Группа</span>
|
||
<span class="kt-ai-info-value">${position.groupName}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Характеристики</span>
|
||
<span class="kt-ai-info-value">${position.specs}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Срок поставки</span>
|
||
<span class="kt-ai-info-value">${position.deadline}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Город</span>
|
||
<span class="kt-ai-info-value">${position.cityName}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Цена</span>
|
||
<span class="kt-ai-info-value">${formatPrice(position.price)} ₸</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Количество</span>
|
||
<span class="kt-ai-info-value">${position.qty} шт.</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Поставщик</span>
|
||
<span class="kt-ai-info-value">${position.supplier}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Совместимость</span>
|
||
<span class="kt-ai-info-value">${getCompatChip(position.compat)}</span>
|
||
</div>
|
||
<div class="kt-ai-info-row">
|
||
<span class="kt-ai-info-label">Статус</span>
|
||
<span class="kt-ai-info-value">${getStatusChip(position.status)}</span>
|
||
</div>
|
||
`;
|
||
|
||
// Сравнение характеристик
|
||
const compareGrid = document.getElementById('compare-grid');
|
||
const specs = position.specs.split(',').map(s => s.trim());
|
||
compareGrid.innerHTML = specs.map(spec => `
|
||
<div class="kt-ai-compare-item">
|
||
<div class="label">Параметр</div>
|
||
<div class="value">${spec}</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
document.getElementById('decision-comment').value = position.comment || '';
|
||
document.getElementById('modal-overlay').classList.add('in');
|
||
}
|
||
|
||
// Закрытие модального окна
|
||
function closeModal() {
|
||
document.getElementById('modal-overlay').classList.remove('in');
|
||
currentPositionId = null;
|
||
}
|
||
|
||
// Сохранение решения
|
||
function makeDecision(decision) {
|
||
if (!currentPositionId) return;
|
||
|
||
const comment = document.getElementById('decision-comment').value.trim();
|
||
saveDecision(currentPositionId, decision, comment);
|
||
|
||
closeModal();
|
||
updateDisplay();
|
||
}
|
||
|
||
// Массовое согласование совместимых
|
||
function approveAll() {
|
||
const data = getAllData();
|
||
const compatible = data.filter(item => item.compat === 'compatible' && item.status === 'review');
|
||
|
||
let count = 0;
|
||
compatible.forEach(item => {
|
||
saveDecision(item.id, 'approved', 'Автоматически согласовано как совместимое');
|
||
count++;
|
||
});
|
||
|
||
document.getElementById('bulk-result').textContent = `Согласовано ${count} позиций`;
|
||
updateDisplay();
|
||
}
|
||
|
||
// Исключение несовместимых
|
||
function excludeIncompatible() {
|
||
const data = getAllData();
|
||
const incompatible = data.filter(item => item.compat === 'incompatible' && item.status !== 'excluded');
|
||
|
||
let count = 0;
|
||
incompatible.forEach(item => {
|
||
saveDecision(item.id, 'excluded', 'Исключено как несовместимое');
|
||
count++;
|
||
});
|
||
|
||
document.getElementById('bulk-result').textContent = `Исключено ${count} несовместимых позиций`;
|
||
updateDisplay();
|
||
}
|
||
|
||
// Выгрузка в Excel (имитация)
|
||
function exportToExcel() {
|
||
const data = getAllData();
|
||
const filtered = filterData(data, currentFilters);
|
||
|
||
// Создаём простой CSV (Excel откроет)
|
||
const headers = ['ID', 'Группа', 'Наименование', 'Характеристики', 'Срок', 'Город', 'Цена', 'Кол-во', 'Поставщик', 'Совместимость', 'Статус', 'Решение', 'Комментарий'];
|
||
const rows = filtered.map(item => [
|
||
item.id,
|
||
item.groupName,
|
||
item.name,
|
||
item.specs,
|
||
item.deadline,
|
||
item.cityName,
|
||
item.price,
|
||
item.qty,
|
||
item.supplier,
|
||
item.compatName,
|
||
item.statusName,
|
||
item.decision || '',
|
||
item.comment || ''
|
||
]);
|
||
|
||
const csv = [headers, ...rows].map(row => row.join(';')).join('\n');
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = 'plan-zakupok.csv';
|
||
link.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Обработчики фильтров
|
||
document.getElementById('filter-group').addEventListener('change', (e) => {
|
||
currentFilters.group = e.target.value;
|
||
updateDisplay();
|
||
});
|
||
|
||
document.getElementById('filter-status').addEventListener('change', (e) => {
|
||
currentFilters.status = e.target.value;
|
||
updateDisplay();
|
||
});
|
||
|
||
document.getElementById('filter-city').addEventListener('change', (e) => {
|
||
currentFilters.city = e.target.value;
|
||
updateDisplay();
|
||
});
|
||
|
||
document.getElementById('filter-compat').addEventListener('change', (e) => {
|
||
currentFilters.compat = 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-exclude').addEventListener('click', () => makeDecision('excluded'));
|
||
|
||
// Инициализация
|
||
updateDisplay();
|
||
|
||
// Сделать функции доступными глобально
|
||
window.openModal = openModal;
|
||
window.toggleGroup = toggleGroup;
|
||
window.approveAll = approveAll;
|
||
window.excludeIncompatible = excludeIncompatible;
|
||
window.exportToExcel = exportToExcel;
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|