pasport-prochital-ya-vybral/index.html

661 lines
32 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Годовой план закупок | Казахтелеком</title>
<link rel="stylesheet" href="./design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="./design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="./design-system/kt-ai-components.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
<style>
.kt-ai-app { min-height: 100vh; 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">&times;</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>