494 lines
24 KiB
HTML
494 lines
24 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">
|
||
<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) {
|
||
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() {
|
||
localStorage.setItem('gpz_comparison_state', JSON.stringify({ groups: DEMO_GROUPS }));
|
||
}
|
||
|
||
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>
|