poprobuy-esche-raz-sobrat-ek/index.html

506 lines
25 KiB
HTML
Raw 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">
<style>
a.kt-ai-btn { text-decoration: none; }
body { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
.app-layout { display: grid; grid-template-columns: 320px 1fr; gap: var(--kt-ai-space-6); padding: var(--kt-ai-space-6); max-width: 1600px; margin: 0 auto; }
.groups-panel { 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); height: calc(100vh - 140px); overflow-y: auto; }
.comparison-panel { 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-item { padding: var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-lg); cursor: pointer; border: 1px solid transparent; margin-bottom: var(--kt-ai-space-2); }
.group-item:hover { background: var(--kt-ai-bg-soft); }
.group-item.active { border-color: var(--kt-ai-primary); background: var(--kt-ai-bg-soft); }
.group-item-title { font-weight: var(--kt-ai-weight-medium); font-size: var(--kt-ai-text-sm); margin-bottom: 4px; }
.group-item-meta { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.table-container { overflow-x: auto; }
.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); 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); border-bottom: 1px solid var(--kt-ai-border-strong); vertical-align: top; }
.kt-ai-table tbody tr:hover { background: var(--kt-ai-bg-soft); }
.kt-ai-table tbody tr.excluded { opacity: 0.5; background: var(--kt-ai-bg-soft); }
.kt-ai-table tbody tr.excluded td { text-decoration: line-through; }
.table-num { text-align: right; font-variant-numeric: tabular-nums; }
.actions-cell { white-space: nowrap; }
.btn-row { display: flex; gap: var(--kt-ai-space-2); flex-wrap: wrap; }
.kpi-strip { display: flex; gap: var(--kt-ai-space-4); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-5); }
.kpi-card { background: var(--kt-ai-bg-soft); padding: var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-xl); min-width: 140px; }
.kpi-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
.kpi-label { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: 2px; }
.filters-row { display: flex; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-4); flex-wrap: wrap; }
.search-input { min-width: 200px; }
.comment-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }
.comment-modal.open { display: flex; }
.comment-box { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); width: 100%; max-width: 480px; }
.comment-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.comment-textarea { width: 100%; min-height: 100px; margin-bottom: var(--kt-ai-space-4); }
.modal-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; }
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
.status-chip { display: inline-block; padding: 2px 8px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-ok { background: var(--kt-ai-status-ok-fg); color: white; }
.status-warn { background: var(--kt-ai-status-warn-fg); color: white; }
.status-risk { background: var(--kt-ai-status-risk-fg); color: white; }
.header-brand { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
.textarea-resize { resize: vertical; }
</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="header-brand">
<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="app-layout">
<!-- Левая панель: Список групп -->
<aside class="groups-panel">
<div style="margin-bottom: var(--kt-ai-space-4)">
<h2 style="font-size: var(--kt-ai-text-lg); margin-bottom: var(--kt-ai-space-2)">Группы похожих позиций</h2>
<p style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">Выберите группу для сравнения</p>
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-4)">
<input class="kt-ai-input" id="groupSearch" placeholder="Поиск группы..." style="width: 100%">
</div>
<div id="groupsList"></div>
</aside>
<!-- Правая панель: Сравнение -->
<section class="comparison-panel">
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-value" id="kpiTotal">0</div>
<div class="kpi-label">Всего строк</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiConfirmed">0</div>
<div class="kpi-label">Подтверждено</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiRejected">0</div>
<div class="kpi-label">Отклонено</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiExcluded">0</div>
<div class="kpi-label">Исключено</div>
</div>
</div>
<div class="filters-row">
<div class="kt-ai-field" style="margin: 0; flex: 1; min-width: 200px;">
<input class="kt-ai-input search-input" id="rowSearch" placeholder="Поиск по наименованию...">
</div>
<div class="kt-ai-field" style="margin: 0;">
<select class="kt-ai-select" id="statusFilter">
<option value="all">Все статусы</option>
<option value="pending">В работе</option>
<option value="confirmed">Подтверждено</option>
<option value="rejected">Отклонено</option>
<option value="excluded">Исключено</option>
</select>
</div>
</div>
<div id="comparisonContent">
<div class="empty-state">
<p>Выберите группу слева для просмотра позиций</p>
</div>
</div>
</section>
</main>
<!-- Модальное окно комментария -->
<div class="comment-modal" id="commentModal">
<div class="comment-box">
<div class="comment-title">Причина отклонения</div>
<textarea class="kt-ai-textarea comment-textarea" id="commentText" placeholder="Опишите, почему позиция не подходит..."></textarea>
<div class="modal-actions">
<button class="kt-ai-btn" data-variant="ghost" onclick="closeCommentModal()">Отмена</button>
<button class="kt-ai-btn" data-variant="primary" onclick="submitReject()">Отклонить</button>
</div>
</div>
</div>
<script>
// Демо-данные: группы похожих позиций
const DEMO_GROUPS = [
{
id: 'g1',
title: 'Ноутбуки для офиса',
category: 'Техника',
groupComment: '',
rows: [
{ id: 'r1', name: 'Ноутбук HP ProBook 450 G9', specs: '15.6" FHD, i5-1235U, 8GB, 256GB SSD', quantity: 15, unit: 'шт', price: 385000, deliveryDate: '2026-10-15', deliveryPlace: 'г. Алматы, ул. Абая 125', status: 'pending', comment: '' },
{ id: 'r2', name: 'Ноутбук Lenovo ThinkPad E15', specs: '15.6" FHD, i5-1235U, 8GB, 512GB SSD', quantity: 10, unit: 'шт', price: 395000, deliveryDate: '2026-10-20', deliveryPlace: 'г. Астана, пр. Туран 30', status: 'pending', comment: '' },
{ id: 'r3', name: 'Ноутбук Dell Latitude 3520', specs: '15.6" FHD, i5-1135G7, 16GB, 512GB SSD', quantity: 8, unit: 'шт', price: 420000, deliveryDate: '2026-11-01', deliveryPlace: 'г. Алматы, ул. Абая 125', status: 'pending', comment: '' },
{ id: 'r4', name: 'Ноутбук ASUS ExpertBook B1', specs: '15.6" FHD, i3-1215U, 8GB, 256GB SSD', quantity: 20, unit: 'шт', price: 310000, deliveryDate: '2026-10-25', deliveryPlace: 'г. Шымкент, ул. Толе би 45', status: 'pending', comment: '' }
]
},
{
id: 'g2',
title: 'Офисные кресла',
category: 'Мебель',
groupComment: '',
rows: [
{ id: 'r5', name: 'Кресло офисное CH-W868', specs: 'Эргономичное, сетка, регулировка высоты', quantity: 50, unit: 'шт', price: 45000, deliveryDate: '2026-09-30', deliveryPlace: 'г. Алматы, ул. Абая 125', status: 'confirmed', comment: 'Оптимальное соотношение цена/качество' },
{ id: 'r6', name: 'Кресло руководителя Samurai', specs: 'Премиум, полная эргономика, подголовник', quantity: 5, unit: 'шт', price: 185000, deliveryDate: '2026-10-10', deliveryPlace: 'г. Астана, пр. Туран 30', status: 'pending', comment: '' },
{ id: 'r7', name: 'Кресло офисное T-9.1', specs: 'Ткань, регулировка наклона, подлокотники', quantity: 30, unit: 'шт', price: 38000, deliveryDate: '2026-10-05', deliveryPlace: 'г. Алматы, ул. Абая 125', status: 'rejected', comment: 'Недостаточная грузоподъёмность' }
]
},
{
id: 'g3',
title: 'Мониторы 24-27"',
category: 'Техника',
groupComment: '',
rows: [
{ id: 'r8', name: 'Монитор Dell P2422H', specs: '24" IPS, FHD, 75Hz, HDMI/DP', quantity: 25, unit: 'шт', price: 78000, deliveryDate: '2026-10-18', deliveryPlace: 'г. Алматы, ул. Абая 125', status: 'pending', comment: '' },
{ id: 'r9', name: 'Монитор LG 27MK430H', specs: '27" IPS, FHD, 75Hz, HDMI', quantity: 15, unit: 'шт', price: 72000, deliveryDate: '2026-10-22', deliveryPlace: 'г. Астана, пр. Туран 30', status: 'pending', comment: '' },
{ id: 'r10', name: 'Монитор HP M27f', specs: '27" IPS, FHD, 75Hz, HDMI/DP', quantity: 20, unit: 'шт', price: 85000, deliveryDate: '2026-11-05', deliveryPlace: 'г. Шымкент, ул. Толе би 45', status: 'excluded', comment: '' }
]
},
{
id: 'g4',
title: 'Серверное оборудование',
category: 'Инфраструктура',
groupComment: '',
rows: [
{ id: 'r11', name: 'Сервер Huawei RH2288H V6', specs: '2x Silver 4314, 64GB, 4x1.2TB HDD, 2x750W', quantity: 2, unit: 'шт', price: 2850000, deliveryDate: '2026-11-15', deliveryPlace: 'г. Алматы, ЦОД ул. Абая 127', status: 'pending', comment: '' },
{ id: 'r12', name: 'Сервер Lenovo ThinkSystem SR650', specs: '2x Gold 5317, 128GB, 4x2TB SSD, 2x1000W', quantity: 1, unit: 'шт', price: 4200000, deliveryDate: '2026-12-01', deliveryPlace: 'г. Астана, ЦОД пр. Туран 32', status: 'pending', comment: '' }
]
},
{
id: 'g5',
title: 'Кабельная продукция',
category: 'Материалы',
groupComment: '',
rows: [
{ id: 'r13', name: 'Кабель UTP 4PR 24AWG', specs: 'Категория 5e, бухта 305м', quantity: 10, unit: 'бухт', price: 45000, deliveryDate: '2026-09-25', deliveryPlace: 'г. Алматы, склад ул. Жарокова 215', status: 'confirmed', comment: 'Стандарт для ЛВС' },
{ id: 'r14', name: 'Кабель FTP 4PR 24AWG', specs: 'Категория 5e, экранированный, 305м', quantity: 5, unit: 'бухт', price: 58000, deliveryDate: '2026-09-28', deliveryPlace: 'г. Астана, склад пр. Сарыарка 15', status: 'pending', comment: '' },
{ id: 'r15', name: 'Кабель UTP 4PR 23AWG', specs: 'Категория 6, бухта 305м', quantity: 8, unit: 'бухт', price: 72000, deliveryDate: '2026-10-08', deliveryPlace: 'г. Алматы, склад ул. Жарокова 215', status: 'pending', comment: '' }
]
}
];
// Загрузка/сохранение в localStorage
function loadState() {
const saved = localStorage.getItem('gpz_comparison_state');
if (saved) {
const state = JSON.parse(saved);
// Merge с демо-данными
DEMO_GROUPS.forEach(g => {
const savedGroup = state.groups?.find(sg => sg.id === g.id);
if (savedGroup) {
// Восстанавливаем комментарий группы
if (savedGroup.groupComment !== undefined) {
g.groupComment = savedGroup.groupComment;
}
// Восстанавливаем статусы и комментарии строк
g.rows.forEach(r => {
const savedRow = savedGroup.rows?.find(sr => sr.id === r.id);
if (savedRow) {
r.status = savedRow.status;
r.comment = savedRow.comment;
}
});
}
});
}
}
function saveState() {
const state = {
groups: DEMO_GROUPS.map(g => ({
id: g.id,
groupComment: g.groupComment,
rows: g.rows.map(r => ({ id: r.id, status: r.status, comment: r.comment }))
}))
};
localStorage.setItem('gpz_comparison_state', JSON.stringify(state));
}
let currentGroupId = null;
let pendingRejectRowId = null;
// Рендер списка групп
function renderGroupsList(filter = '') {
const container = document.getElementById('groupsList');
const filtered = DEMO_GROUPS.filter(g =>
g.title.toLowerCase().includes(filter.toLowerCase()) ||
g.category.toLowerCase().includes(filter.toLowerCase())
);
if (filtered.length === 0) {
container.innerHTML = '<div class="empty-state" style="padding: var(--kt-ai-space-6)"><p>Группы не найдены</p></div>';
return;
}
container.innerHTML = filtered.map(g => {
const total = g.rows.length;
const confirmed = g.rows.filter(r => r.status === 'confirmed').length;
const rejected = g.rows.filter(r => r.status === 'rejected').length;
const excluded = g.rows.filter(r => r.status === 'excluded').length;
const activeClass = g.id === currentGroupId ? 'active' : '';
return `<div class="group-item ${activeClass}" onclick="selectGroup('${g.id}')">
<div class="group-item-title">${g.title}</div>
<div class="group-item-meta">${g.category} • ${total} позиций</div>
${g.groupComment ? `<div class="group-item-meta" style="margin-top: 6px; font-style: italic; color: var(--kt-ai-fg-muted)">💬 ${g.groupComment.substring(0, 50)}${g.groupComment.length > 50 ? '...' : ''}</div>` : ''}
<div class="group-item-meta" style="margin-top: 4px">
<span class="status-chip status-ok" style="background: var(--kt-ai-primary)">✓ ${confirmed}</span>
<span class="status-chip status-risk" style="background: var(--kt-ai-status-risk-fg); margin-left: 4px">✗ ${rejected}</span>
<span class="status-chip" style="background: var(--kt-ai-fg-muted); margin-left: 4px">⊘ ${excluded}</span>
</div>
</div>`;
}).join('');
}
// Выбор группы
function selectGroup(groupId) {
currentGroupId = groupId;
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
// Рендер таблицы сравнения
function renderComparison() {
const container = document.getElementById('comparisonContent');
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
if (!group) {
container.innerHTML = '<div class="empty-state"><p>Выберите группу слева для просмотра позиций</p></div>';
return;
}
const rowSearch = document.getElementById('rowSearch').value.toLowerCase();
const statusFilter = document.getElementById('statusFilter').value;
let filteredRows = group.rows.filter(r => {
const matchesSearch = r.name.toLowerCase().includes(rowSearch) || r.specs.toLowerCase().includes(rowSearch);
const matchesStatus = statusFilter === 'all' || r.status === statusFilter;
return matchesSearch && matchesStatus;
});
if (filteredRows.length === 0) {
container.innerHTML = '<div class="empty-state"><p>Нет позиций по выбранным фильтрам</p></div>';
return;
}
container.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-4)">
<div>
<h2 style="font-size: var(--kt-ai-text-xl); margin-bottom: 4px">${group.title}</h2>
<p style="font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted)">${group.category}</p>
</div>
</div>
<div class="kt-ai-field" style="margin-bottom: var(--kt-ai-space-5)">
<label style="font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); margin-bottom: var(--kt-ai-space-2); display: block">Комментарий к группе</label>
<textarea class="kt-ai-textarea" id="groupCommentInput" placeholder="Почему эту группу стоит подтвердить или отклонить..." style="min-height: 60px">${group.groupComment || ''}</textarea>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="saveGroupComment()" style="margin-top: var(--kt-ai-space-2)">Сохранить комментарий</button>
</div>
<div class="table-container">
<table class="kt-ai-table">
<thead>
<tr>
<th>Наименование</th>
<th>Характеристики</th>
<th class="table-num">Количество</th>
<th>Ед.</th>
<th class="table-num">Цена (₸)</th>
<th>Срок поставки</th>
<th>Место поставки</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
${filteredRows.map(r => {
const excludedClass = r.status === 'excluded' ? 'excluded' : '';
const statusChip = getStatusChip(r.status);
return `<tr class="${excludedClass}" data-row-id="${r.id}">
<td style="min-width: 200px">${r.name}</td>
<td style="min-width: 180px; color: var(--kt-ai-fg-muted)">${r.specs}</td>
<td class="table-num">${r.quantity}</td>
<td>${r.unit}</td>
<td class="table-num">${r.price.toLocaleString('ru-RU')}</td>
<td>${formatDate(r.deliveryDate)}</td>
<td>${r.deliveryPlace}</td>
<td>${statusChip}</td>
<td class="actions-cell">
<div class="btn-row">
${r.status !== 'excluded' ? `<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="excludeRow('${r.id}')">Исключить</button>` : ''}
${r.status !== 'confirmed' ? `<button class="kt-ai-btn" data-variant="primary" data-size="sm" onclick="confirmRow('${r.id}')">✓</button>` : ''}
${r.status !== 'rejected' ? `<button class="kt-ai-btn" data-variant="danger" data-size="sm" onclick="openRejectModal('${r.id}')">✗</button>` : ''}
</div>
${r.comment ? `<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: 4px; max-width: 250px">💬 ${r.comment}</div>` : ''}
</td>
</tr>`;
}).join('')}
</tbody>
</table>
</div>
<div style="display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-5); justify-content: flex-end">
<button class="kt-ai-btn" data-variant="ghost" onclick="resetGroup()">Сбросить</button>
<button class="kt-ai-btn" data-variant="primary" onclick="confirmGroup()">Подтвердить группу</button>
</div>
`;
updateKPI(group);
}
function getStatusChip(status) {
const map = {
'pending': '<span class="status-chip" style="background: var(--kt-ai-fg-muted)">В работе</span>',
'confirmed': '<span class="status-chip status-ok">✓ Подтверждено</span>',
'rejected': '<span class="status-chip status-risk">✗ Отклонено</span>',
'excluded': '<span class="status-chip" style="background: var(--kt-ai-fg-faint)">⊘ Исключено</span>'
};
return map[status] || map['pending'];
}
function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
}
function updateKPI(group) {
document.getElementById('kpiTotal').textContent = group.rows.length;
document.getElementById('kpiConfirmed').textContent = group.rows.filter(r => r.status === 'confirmed').length;
document.getElementById('kpiRejected').textContent = group.rows.filter(r => r.status === 'rejected').length;
document.getElementById('kpiExcluded').textContent = group.rows.filter(r => r.status === 'excluded').length;
}
// Действия со строками
function excludeRow(rowId) {
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
const row = group.rows.find(r => r.id === rowId);
if (row) {
row.status = 'excluded';
row.comment = '';
saveState();
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
}
function confirmRow(rowId) {
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
const row = group.rows.find(r => r.id === rowId);
if (row) {
row.status = 'confirmed';
row.comment = '';
saveState();
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
}
function openRejectModal(rowId) {
pendingRejectRowId = rowId;
document.getElementById('commentText').value = '';
document.getElementById('commentModal').classList.add('open');
document.getElementById('commentText').focus();
}
function closeCommentModal() {
document.getElementById('commentModal').classList.remove('open');
pendingRejectRowId = null;
}
function submitReject() {
const comment = document.getElementById('commentText').value.trim();
if (!comment) {
alert('Введите причину отклонения');
return;
}
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
const row = group.rows.find(r => r.id === pendingRejectRowId);
if (row) {
row.status = 'rejected';
row.comment = comment;
saveState();
closeCommentModal();
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
}
function confirmGroup() {
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
group.rows.forEach(r => {
if (r.status !== 'excluded' && r.status !== 'rejected') {
r.status = 'confirmed';
}
});
saveState();
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
function resetGroup() {
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
group.rows.forEach(r => {
r.status = 'pending';
r.comment = '';
});
group.groupComment = '';
saveState();
renderGroupsList(document.getElementById('groupSearch').value);
renderComparison();
}
function saveGroupComment() {
const group = DEMO_GROUPS.find(g => g.id === currentGroupId);
const comment = document.getElementById('groupCommentInput').value.trim();
if (group) {
group.groupComment = comment;
saveState();
renderGroupsList(document.getElementById('groupSearch').value);
}
}
// Event listeners
document.getElementById('groupSearch').addEventListener('input', e => {
renderGroupsList(e.target.value);
});
document.getElementById('rowSearch').addEventListener('input', () => {
renderComparison();
});
document.getElementById('statusFilter').addEventListener('change', () => {
renderComparison();
});
// Close modal on backdrop click
document.getElementById('commentModal').addEventListener('click', e => {
if (e.target.id === 'commentModal') closeCommentModal();
});
// Init
loadState();
renderGroupsList();
// Keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeCommentModal();
});
</script>
</body>
</html>