pervogo-maketa-opisanie-podh/index.html

383 lines
19 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>Сравнение закупок — 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); }
.groups-strip { display: flex; flex-direction: column; gap: var(--kt-ai-space-5); margin-top: var(--kt-ai-space-6); }
.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); }
.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); }
.group-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); }
.group-meta { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.comparison-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-4); }
.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; }
.compare-card.duplicate { border-color: var(--kt-ai-chart-red); background: rgba(255, 235, 238, 0.5); }
.compare-card.selected { border-color: var(--kt-ai-chart-blue); background: rgba(230, 244, 250, 0.5); }
.compare-card.excluded { opacity: 0.5; }
.compare-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-3); }
.compare-id { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.compare-name { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); }
.compare-specs { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-3); }
.spec-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px dashed var(--kt-ai-border); }
.spec-row:last-child { border-bottom: none; }
.spec-label { color: var(--kt-ai-fg-muted); }
.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); }
</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>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="exportToExcel()">
📥 Excel
</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top: var(--kt-ai-space-6); padding-bottom: var(--kt-ai-space-8);">
<!-- Фильтры -->
<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">
<select class="kt-ai-select" id="filter-status" onchange="applyFilters()">
<option value="all">Все статусы</option>
<option value="review">На рассмотрении</option>
<option value="decided">Решение принято</option>
</select>
</div>
<span class="kt-ai-spacer"></span>
<span style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted);">
Найдено групп: <strong id="groups-count">0</strong>
</span>
</div>
<!-- Группы закупок для сравнения -->
<div id="groups-container" class="groups-strip"></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 class="summary-strip">
<div class="summary-card">
<div class="summary-value" id="summary-total">0</div>
<div class="summary-label">Всего строк</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>
</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_GROUPS = [
{
id: 'grp-001',
name: 'Серверы',
status: 'review',
items: [
{ 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-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-023', name: 'Серверная платформа (базовая)', specs: { 'CPU': '2x Intel Xeon Silver', 'RAM': '128 GB', 'Storage': '2x 960GB' }, sum: 8200000, isDuplicate: false, isExcluded: true, excludeReason: 'Не подходит по характеристикам' }
],
decision: { selected: 'ZK-2026-001', comment: '' }
},
{
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 FILTERS = [
{ id: 'group', label: 'Группа', affects: ['groups'] },
{ id: 'status', label: 'Статус', affects: ['groups'] }
];
let currentGroups = [...DEMO_GROUPS];
function formatSum(sum) {
return sum.toLocaleString('ru-KZ') + ' ₸';
}
function renderGroups(groups) {
const container = document.getElementById('groups-container');
const emptyState = document.getElementById('empty-state');
const groupsCount = document.getElementById('groups-count');
if (groups.length === 0) {
container.innerHTML = '';
emptyState.style.display = 'block';
groupsCount.textContent = '0';
return;
}
emptyState.style.display = 'none';
groupsCount.textContent = groups.length;
container.innerHTML = groups.map(group => {
const itemsHtml = group.items.map((item, idx) => {
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('');
const hasDecision = group.decision.selected || group.items.some(i => i.isExcluded);
const statusChip = group.status === 'decided'
? '<span class="kt-ai-chip" data-tone="green">Решение принято</span>'
: '<span class="kt-ai-chip" data-tone="orange">На рассмотрении</span>';
const decisionComment = group.decision.comment
? `<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>`
: '';
return `
<div class="group-card">
<div class="group-head">
<span class="group-title">${group.name}</span>
${statusChip}
<span class="kt-ai-spacer"></span>
<span class="group-meta">${group.items.length} строк</span>
</div>
<div class="comparison-grid">${itemsHtml}</div>
<div class="decision-box">
<div class="decision-label">Комментарий к решению:</div>
<textarea class="kt-ai-textarea" placeholder="Введите обоснование выбора..." onchange="updateComment('${group.id}', this.value)">${group.decision.comment}</textarea>
${decisionComment}
</div>
</div>
`;
}).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() {
const groupFilter = document.getElementById('filter-group').value;
const statusFilter = document.getElementById('filter-status').value;
let filtered = [...DEMO_GROUPS];
if (groupFilter !== 'all') {
filtered = filtered.filter(g => g.id === groupFilter || g.name.toLowerCase().includes(groupFilter));
}
if (statusFilter !== 'all') {
filtered = filtered.filter(g => g.status === statusFilter);
}
currentGroups = filtered;
renderGroups(filtered);
}
function exportToExcel() {
let csv = '\uFEFF';
csv += 'Группа;ID;Наименование;Характеристики;Сумма;Статус;Выбрана;Исключена;Причина\n';
currentGroups.forEach(g => {
g.items.forEach(i => {
const specs = Object.entries(i.specs).map(([k,v]) => `${k}: ${v}`).join('; ');
const isSelected = i.id === g.decision.selected ? 'Да' : 'Нет';
const isExcluded = i.isExcluded ? 'Да' : 'Нет';
csv += `${g.name};${i.id};"${i.name}";"${specs}";${i.sum};${g.status};${isSelected};${isExcluded};"${i.excludeReason}"\n`;
});
});
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sravnenie_zakupok.csv';
a.click();
URL.revokeObjectURL(url);
}
// Инициализация
renderGroups(DEMO_GROUPS);
</script>
</body>
</html>