poprobuy-esche-raz-ekran-raz/index.html

582 lines
30 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">
<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); min-height: 100vh; }
.app-header { display: flex; align-items: center; gap: var(--kt-ai-space-4); padding: var(--kt-ai-space-4) 0; }
.app-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.filters-panel { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-6); }
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.groups-list { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); }
.group-card { border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); background: var(--kt-ai-card-bg); }
.group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-3); }
.group-items { display: flex; flex-direction: column; gap: var(--kt-ai-space-2); margin: var(--kt-ai-space-3) 0; }
.group-item { display: grid; grid-template-columns: auto 1.8fr auto auto auto; gap: var(--kt-ai-space-3); align-items: start; padding: var(--kt-ai-space-3); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); }
.group-item.selected { background: var(--kt-ai-bg-soft); border-color: var(--kt-ai-primary); }
.item-checkbox { width: 18px; height: 18px; cursor: pointer; }
.item-name { font-size: var(--kt-ai-text-sm); }
.item-code { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.item-amount { text-align: right; font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-sm); tabular-nums: tabular-nums; }
.item-category { font-size: var(--kt-ai-text-xs); }
.item-actions { display: flex; gap: var(--kt-ai-space-2); }
.group-actions { display: flex; gap: var(--kt-ai-space-3); align-items: center; margin-top: var(--kt-ai-space-4); padding-top: var(--kt-ai-space-4); border-top: 1px solid var(--kt-ai-border); }
.comment-field { flex: 1; }
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-6); color: var(--kt-ai-fg-muted); }
.empty-icon { font-size: 48px; margin-bottom: var(--kt-ai-space-4); }
.status-chip { min-width: 100px; justify-self: end; }
.selection-panel { position: sticky; bottom: var(--kt-ai-space-4); background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-6); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.selection-info { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--kt-ai-space-3); }
</style>
</head>
<body class="kt-ai-app" data-kt-data-mode="demo">
<div class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-6);padding-bottom:var(--kt-ai-space-8)">
<header class="app-header">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<span class="app-title">Разбор похожих позиций</span>
<span class="kt-ai-spacer"></span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
</header>
<div class="kpi-strip" style="margin-top:var(--kt-ai-space-6)">
<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-processed" 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-remaining" style="color:var(--kt-ai-status-warn-fg)">0</div>
<div class="label">Осталось</div>
<div class="hint">Требует разбора</div>
</div>
<div class="kt-ai-kpi">
<div class="value" id="kpi-sum" style="color:var(--kt-ai-chart-blue)">0 ₸</div>
<div class="label">Сумма в работе</div>
<div class="hint">По выбранным группам</div>
</div>
</div>
<div class="filters-panel">
<div class="kt-ai-field" style="margin:0">
<label>Категория</label>
<select class="kt-ai-select" id="filter-category">
<option value="all">Все категории</option>
<option value="it">IT и оборудование</option>
<option value="services">Услуги</option>
<option value="materials">Материалы</option>
<option value="construction">Строительство</option>
</select>
</div>
<div class="kt-ai-field" style="margin:0">
<label>Статус</label>
<select class="kt-ai-select" id="filter-status">
<option value="all">Все статусы</option>
<option value="new">Новые</option>
<option value="review">На проверке</option>
<option value="confirmed">Подтверждено</option>
<option value="rejected">Отклонено</option>
</select>
</div>
<div class="kt-ai-field" style="margin:0">
<label>Период</label>
<select class="kt-ai-select" id="filter-period">
<option value="all">Весь период</option>
<option value="2026">2026 год</option>
<option value="2025">2025 год</option>
<option value="q1">Q1 2026</option>
<option value="q2">Q2 2026</option>
<option value="q3">Q3 2026</option>
<option value="q4">Q4 2026</option>
</select>
<span data-kt-filter-note style="font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted);margin-left:var(--kt-ai-space-2)">в демо не меняет набор</span>
</div>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="btn-reset">Сбросить фильтры</button>
<button class="kt-ai-btn" data-variant="ghost" id="btn-clear-storage" title="Очистить сохранённые данные и загрузить демо заново">Сброс данных</button>
</div>
<div id="empty-state" class="empty-state" style="display:none">
<div class="empty-icon" id="empty-icon">✅</div>
<h3 style="margin-bottom:var(--kt-ai-space-2)" id="empty-title">Все позиции разобраны</h3>
<p id="empty-text">Нет групп для обработки с выбранными фильтрами</p>
</div>
<div class="groups-list" id="groups-container"></div>
<div class="selection-panel" id="selection-panel" style="display:none">
<div class="selection-info">
<strong>Выбрано позиций: <span id="selected-count">0</span></strong>
<span class="kt-ai-chip" id="selection-status">Готово к обработке</span>
</div>
<div class="kt-ai-field comment-field">
<label>Комментарий к решению</label>
<textarea class="kt-ai-textarea" id="group-comment" rows="2" placeholder="Укажите причину подтверждения или отклонения..."></textarea>
</div>
<div style="display:flex;gap:var(--kt-ai-space-3);margin-top:var(--kt-ai-space-3)">
<button class="kt-ai-btn" data-variant="danger" id="btn-reject">Отклонить выбранные</button>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="primary" id="btn-confirm">Подтвердить выбранные</button>
</div>
</div>
</div>
<script>
// Реестр фильтров по требованию брифа
const FILTERS = [
{ id: 'category', label: 'Категория', affects: ['table', 'kpi'] },
{ id: 'status', label: 'Статус', affects: ['table', 'kpi'] },
{ id: 'period', label: 'Период', affects: ['table'], demoOnly: true }
];
// Демо-данные: группы похожих позиций годового плана закупок
const DEMO_DATA = [
{
id: 'group-1',
status: 'new',
items: [
{ code: 'ZKP-2026-00142', name: 'Поставка компьютерного оборудования для нужд департамента', amount: 2450000, category: 'it', year: '2026', quarter: 'q1', specs: 'ПК: Core i5, 16GB, 512GB SSD; Мониторы 24"', deliveryDate: '15.04.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, склад №3' },
{ code: 'ZKP-2026-00189', name: 'Закупка компьютерной техники для офисов', amount: 2380000, category: 'it', year: '2026', quarter: 'q1', specs: 'Ноутбуки: Core i5, 8GB, 256GB SSD; Мыши, клавиатуры', deliveryDate: '20.04.2026', deliveryPlace: 'г. Астана, ул. Сыганак 23, офис 401' },
{ code: 'ZKP-2026-00201', name: 'Поставка ПК и периферийного оборудования', amount: 2510000, category: 'it', year: '2026', quarter: 'q1', specs: 'Моноблоки 24", Core i3, 8GB, 1TB HDD; Принтеры МФУ', deliveryDate: '10.05.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, склад №3' }
]
},
{
id: 'group-2',
status: 'new',
items: [
{ code: 'ZKP-2026-00312', name: 'Услуги по технической поддержке программного обеспечения', amount: 890000, category: 'services', year: '2026', quarter: 'q2', specs: 'ПО: 1С, Windows Server, антивирусы; 12 месяцев', deliveryDate: '01.06.2026', deliveryPlace: 'Удалённая поддержка + выезд в офисы' },
{ code: 'ZKP-2026-00345', name: 'Техническая поддержка и сопровождение ПО', amount: 920000, category: 'services', year: '2026', quarter: 'q2', specs: 'Сопровождение: 1С, CRM, документооборот; 12 месяцев', deliveryDate: '15.06.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, серверная' }
]
},
{
id: 'group-3',
status: 'new',
items: [
{ code: 'ZKP-2026-00478', name: 'Приобретение кабельной продукции для магистральных линий', amount: 15600000, category: 'materials', year: '2026', quarter: 'q2', specs: 'Кабель оптический 48 волокон, 12 км; муфты, кроссы', deliveryDate: '10.07.2026', deliveryPlace: 'г. Шымкент, ул. Желтоксан 45, склад связи' },
{ code: 'ZKP-2026-00489', name: 'Поставка кабеля и проводников для сетей связи', amount: 15200000, category: 'materials', year: '2026', quarter: 'q2', specs: 'Кабель медный UTP cat.6, 25 км; коннекторы RJ45', deliveryDate: '25.07.2026', deliveryPlace: 'г. Тараз, пр. Толе би 89, РТС' },
{ code: 'ZKP-2026-00501', name: 'Кабельно-проводниковая продукция для строительства ЛЭП', amount: 14900000, category: 'materials', year: '2026', quarter: 'q2', specs: 'Провод СИП-3 70мм², 8 км; арматура, изоляторы', deliveryDate: '05.08.2026', deliveryPlace: 'г. Кызылорда, ул. Байтурсынова 12, склад №7' }
]
},
{
id: 'group-4',
status: 'new',
items: [
{ code: 'ZKP-2026-00623', name: 'Строительно-монтажные работы по реконструкции здания', amount: 42000000, category: 'construction', year: '2026', quarter: 'q1', specs: 'Реконструкция 2 этажа, 850 м²; фасад, кровля, инженерия', deliveryDate: '30.09.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, здание А' },
{ code: 'ZKP-2026-00634', name: 'Реконструкция административного здания с капитальным ремонтом', amount: 43500000, category: 'construction', year: '2026', quarter: 'q1', specs: 'Капремонт 1-3 этажей, 1200 м²; замена коммуникаций', deliveryDate: '15.10.2026', deliveryPlace: 'г. Астана, ул. Сыганак 23, корпус 2' }
]
},
{
id: 'group-5',
status: 'new',
items: [
{ code: 'ZKP-2025-01234', name: 'Услуги уборки служебных помещений', amount: 3200000, category: 'services', year: '2025', quarter: 'q4', specs: 'Ежедневная уборка, 2500 м²; моющие средства включены', deliveryDate: '01.12.2025', deliveryPlace: 'г. Алматы, пр. Абая 125, все здания' },
{ code: 'ZKP-2025-01256', name: 'Клининговые услуги для офисных площадей', amount: 3100000, category: 'services', year: '2025', quarter: 'q4', specs: 'Комплексная уборка, 2200 м²; химчистка мебели', deliveryDate: '10.12.2025', deliveryPlace: 'г. Астана, ул. Сыганак 23, офисы 3-5 этажи' }
]
},
{
id: 'group-6',
status: 'new',
items: [
{ code: 'ZKP-2026-00789', name: 'Поставка серверного оборудования и систем хранения', amount: 28700000, category: 'it', year: '2026', quarter: 'q3', specs: 'Серверы 2U: 2xXeon, 128GB, RAID; СХД 100ТБ', deliveryDate: '20.09.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, ЦОД' },
{ code: 'ZKP-2026-00801', name: 'Закупка серверов и СХД для дата-центра', amount: 29100000, category: 'it', year: '2026', quarter: 'q3', specs: 'Серверы 4U: 4xXeon, 256GB, NVMe; СХД 200ТБ FC', deliveryDate: '01.10.2026', deliveryPlace: 'г. Астана, пр. Мангилик Ел 55, ЦОД' }
]
},
{
id: 'group-7',
status: 'new',
items: [
{ code: 'ZKP-2026-00912', name: 'Поставка расходных материалов для оргтехники', amount: 1850000, category: 'materials', year: '2026', quarter: 'q4', specs: 'Картриджи: HP, Canon, Kyocera; бумага А4, 500 пачек', deliveryDate: '15.11.2026', deliveryPlace: 'г. Алматы, пр. Абая 125, склад канцтоваров' },
{ code: 'ZKP-2026-00934', name: 'Закупка тонеров и картриджей для принтеров', amount: 1920000, category: 'materials', year: '2026', quarter: 'q4', specs: 'Тонер-картриджи: Samsung, Xerox, Brother; 300 шт', deliveryDate: '25.11.2026', deliveryPlace: 'г. Шымкент, ул. Желтоксан 45, склад №2' }
]
},
{
id: 'group-8',
status: 'new',
items: [
{ code: 'ZKP-2026-01045', name: 'Услуги охраны объектов связи', amount: 12500000, category: 'services', year: '2026', quarter: 'q1', specs: 'Круглосуточная охрана, 15 объектов; пультовая охрана', deliveryDate: '01.01.2026', deliveryPlace: 'г. Алматы и Алматинская область, 15 РТС' },
{ code: 'ZKP-2026-01067', name: 'Охрана зданий и территорий компании', amount: 12800000, category: 'services', year: '2026', quarter: 'q1', specs: 'Физическая охрана, 18 постов; видеонаблюдение', deliveryDate: '15.01.2026', deliveryPlace: 'г. Астана и области, офисы и склады' }
]
}
];
const CATEGORY_NAMES = {
it: 'IT и оборудование',
services: 'Услуги',
materials: 'Материалы',
construction: 'Строительство'
};
const STATUS_LABELS = {
new: { text: 'Новые', tone: 'blue' },
review: { text: 'На проверке', tone: 'orange' },
confirmed: { text: 'Подтверждено', tone: 'green' },
rejected: { text: 'Отклонено', tone: 'red' }
};
// Состояние приложения
let appState = {
groups: [],
selectedItems: new Set(),
filters: { category: 'all', status: 'all', period: 'all' }
};
// Загрузка из localStorage
function loadState() {
const saved = localStorage.getItem('zakupki-review-state');
if (saved) {
try {
const parsed = JSON.parse(saved);
appState.groups = parsed.groups || [];
appState.filters = parsed.filters || { category: 'all', status: 'all', period: 'all' };
} catch (e) {
appState.groups = [];
}
}
if (appState.groups.length === 0) {
appState.groups = JSON.parse(JSON.stringify(DEMO_DATA));
}
}
// Сохранение в localStorage
function saveState() {
const toSave = {
groups: appState.groups,
filters: appState.filters
};
localStorage.setItem('zakupki-review-state', JSON.stringify(toSave));
}
// Форматирование суммы
function formatAmount(amount) {
return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT', maximumFractionDigits: 0 }).format(amount);
}
// Рендер группы
function renderGroup(group, index) {
const statusInfo = STATUS_LABELS[group.status] || STATUS_LABELS.new;
const isProcessed = group.status === 'confirmed' || group.status === 'rejected';
let itemsHtml = '';
group.items.forEach((item, itemIndex) => {
const isSelected = appState.selectedItems.has(`${group.id}-${itemIndex}`);
itemsHtml += `
<div class="group-item ${isSelected ? 'selected' : ''}" data-group="${group.id}" data-item="${itemIndex}">
<input type="checkbox" class="item-checkbox" ${isSelected ? 'checked' : ''} ${isProcessed ? 'disabled' : ''}>
<div style="min-width:0">
<div class="item-name" style="margin-bottom:var(--kt-ai-space-1)">${item.name}</div>
<div class="item-code">${item.code}</div>
<div style="font-size:var(--kt-ai-text-xs);color:var(--kt-ai-fg-muted);margin-top:var(--kt-ai-space-1)">
${item.specs ? `<div style="margin-bottom:2px"><strong>Характеристики:</strong> ${item.specs}</div>` : ''}
${item.deliveryDate ? `<div style="margin-bottom:2px"><strong>Срок поставки:</strong> ${item.deliveryDate}</div>` : '<div style="margin-bottom:2px"><strong>Срок поставки:</strong> <em>не указан</em></div>'}
${item.deliveryPlace ? `<div><strong>Место:</strong> ${item.deliveryPlace}</div>` : '<div><strong>Место:</strong> <em>не указано</em></div>'}
</div>
</div>
<div class="item-category"><span class="kt-ai-chip" data-tone="gray">${CATEGORY_NAMES[item.category]}</span></div>
<div class="item-amount">${formatAmount(item.amount)}</div>
<div class="status-chip"><span class="kt-ai-chip" data-tone="${statusInfo.tone}">${statusInfo.text}</span></div>
</div>
`;
});
return `
<div class="group-card" data-group-id="${group.id}" style="${isProcessed ? 'opacity:0.6' : ''}">
<div class="group-header">
<strong>Группа ${index + 1} &mdash; ${CATEGORY_NAMES[group.items[0].category]}</strong>
<span class="kt-ai-chip" data-tone="${statusInfo.tone}">${group.items.length} позиций</span>
</div>
<div class="group-items">${itemsHtml}</div>
${!isProcessed ? `
<div class="group-actions">
<span class="kt-ai-chip" data-tone="gray" style="font-size:var(--kt-ai-text-xs)">Похожесть: ${Math.floor(Math.random() * 15 + 85)}%</span>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn kt-ai-btn-ghost btn-reject-group" data-group="${group.id}">Отклонить все</button>
<button class="kt-ai-btn kt-ai-btn-primary btn-confirm-group" data-group="${group.id}">Подтвердить все</button>
</div>
` : `<div style="margin-top:var(--kt-ai-space-3);font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted)">Обработано: ${group.comment || 'без комментария'}</div>`}
</div>
`;
}
// Фильтрация групп
function filterGroups() {
const { category, status, period } = appState.filters;
return appState.groups.filter(group => {
if (group.status === 'confirmed' || group.status === 'rejected') {
if (status !== 'all' && status !== 'confirmed' && status !== 'rejected') return false;
if (status === 'confirmed' && group.status !== 'confirmed') return false;
if (status === 'rejected' && group.status !== 'rejected') return false;
} else {
if (status === 'confirmed' || status === 'rejected') return false;
if (status === 'review') return false;
}
const itemCategory = group.items[0].category;
if (category !== 'all' && itemCategory !== category) return false;
const itemPeriod = group.items[0].quarter;
const itemYear = group.items[0].year;
if (period !== 'all') {
if (period === '2026' && itemYear !== '2026') return false;
if (period === '2025' && itemYear !== '2025') return false;
if (period === 'q1' && itemPeriod !== 'q1') return false;
if (period === 'q2' && itemPeriod !== 'q2') return false;
if (period === 'q3' && itemPeriod !== 'q3') return false;
if (period === 'q4' && itemPeriod !== 'q4') return false;
}
return true;
});
}
// Обновление KPI
function updateKPI() {
const total = appState.groups.length;
const processed = appState.groups.filter(g => g.status === 'confirmed' || g.status === 'rejected').length;
const remaining = total - processed;
const remainingGroups = appState.groups.filter(g => g.status !== 'confirmed' && g.status !== 'rejected');
const sumInWork = remainingGroups.reduce((sum, g) => {
return sum + g.items.reduce((s, i) => s + i.amount, 0);
}, 0);
document.getElementById('kpi-total').textContent = total;
document.getElementById('kpi-processed').textContent = processed;
document.getElementById('kpi-remaining').textContent = remaining;
document.getElementById('kpi-sum').textContent = formatAmount(sumInWork);
}
// Рендер списка групп
function renderGroups() {
const container = document.getElementById('groups-container');
const emptyState = document.getElementById('empty-state');
const emptyTitle = document.getElementById('empty-title');
const emptyText = document.getElementById('empty-text');
const emptyIcon = document.getElementById('empty-icon');
const filtered = filterGroups();
if (filtered.length === 0) {
container.style.display = 'none';
emptyState.style.display = 'block';
document.getElementById('selection-panel').style.display = 'none';
// Определяем причину пустого состояния
const hasProcessedOnly = appState.groups.every(g => g.status === 'confirmed' || g.status === 'rejected');
if (hasProcessedOnly && appState.filters.status === 'all') {
emptyIcon.textContent = '✅';
emptyTitle.textContent = 'Все позиции разобраны';
emptyText.textContent = 'Нет групп для обработки';
} else if (appState.filters.period === 'q3' || appState.filters.period === 'q4') {
emptyIcon.textContent = '📭';
emptyTitle.textContent = 'Нет данных';
emptyText.textContent = `За выбранный период (${document.getElementById('filter-period').options[document.getElementById('filter-period').selectedIndex].text}) данных нет`;
} else if (appState.filters.status !== 'all') {
emptyIcon.textContent = '🔍';
emptyTitle.textContent = 'Ничего не найдено';
emptyText.textContent = `Нет групп со статусом "${document.getElementById('filter-status').options[document.getElementById('filter-status').selectedIndex].text}"`;
} else {
emptyIcon.textContent = '📭';
emptyTitle.textContent = 'Нет данных';
emptyText.textContent = 'Нет групп для обработки с выбранными фильтрами';
}
return;
}
emptyState.style.display = 'none';
container.style.display = 'flex';
let html = '';
filtered.forEach((group, index) => {
html += renderGroup(group, index);
});
container.innerHTML = html;
updateKPI();
attachEventListeners();
}
// Прикрепление обработчиков
function attachEventListeners() {
document.querySelectorAll('.item-checkbox').forEach(cb => {
cb.addEventListener('change', function() {
const groupEl = this.closest('.group-item');
const groupId = groupEl.dataset.group;
const itemIndex = groupEl.dataset.item;
const key = `${groupId}-${itemIndex}`;
if (this.checked) {
appState.selectedItems.add(key);
groupEl.classList.add('selected');
} else {
appState.selectedItems.delete(key);
groupEl.classList.remove('selected');
}
updateSelectionPanel();
});
});
document.querySelectorAll('.btn-reject-group').forEach(btn => {
btn.addEventListener('click', function() {
const groupId = this.dataset.group;
processGroup(groupId, 'rejected', 'Отклонено массово');
});
});
document.querySelectorAll('.btn-confirm-group').forEach(btn => {
btn.addEventListener('click', function() {
const groupId = this.dataset.group;
processGroup(groupId, 'confirmed', 'Подтверждено массово');
});
});
}
// Обновление панели выбора
function updateSelectionPanel() {
const panel = document.getElementById('selection-panel');
const count = appState.selectedItems.size;
if (count === 0) {
panel.style.display = 'none';
return;
}
panel.style.display = 'block';
document.getElementById('selected-count').textContent = count;
// Пересоздаём обработчики кнопок каждый раз
const btnConfirm = document.getElementById('btn-confirm');
const btnReject = document.getElementById('btn-reject');
// Клонируем кнопки, чтобы сбросить старые обработчики
const newConfirm = btnConfirm.cloneNode(true);
const newReject = btnReject.cloneNode(true);
btnConfirm.parentNode.replaceChild(newConfirm, btnConfirm);
btnReject.parentNode.replaceChild(newReject, btnReject);
// Вешаем новые обработчики
newConfirm.addEventListener('click', function() {
if (appState.selectedItems.size === 0) return;
const comment = document.getElementById('group-comment').value || 'Подтверждено пользователем';
const processedGroups = new Set();
appState.selectedItems.forEach(key => {
const [groupId, itemIndex] = key.split('-');
const group = appState.groups.find(g => g.id === groupId);
if (group && group.status === 'new') {
group.status = 'confirmed';
group.comment = comment;
processedGroups.add(groupId);
}
});
appState.selectedItems.clear();
saveState();
renderGroups();
document.getElementById('group-comment').value = '';
});
newReject.addEventListener('click', function() {
if (appState.selectedItems.size === 0) return;
const comment = document.getElementById('group-comment').value || 'Отклонено пользователем';
const processedGroups = new Set();
appState.selectedItems.forEach(key => {
const [groupId, itemIndex] = key.split('-');
const group = appState.groups.find(g => g.id === groupId);
if (group && group.status === 'new') {
group.status = 'rejected';
group.comment = comment;
processedGroups.add(groupId);
}
});
appState.selectedItems.clear();
saveState();
renderGroups();
document.getElementById('group-comment').value = '';
});
}
// Обработка группы
function processGroup(groupId, status, defaultComment) {
const group = appState.groups.find(g => g.id === groupId);
if (!group) return;
const comment = document.getElementById('group-comment').value || defaultComment;
group.status = status;
group.comment = comment;
appState.selectedItems.clear();
saveState();
renderGroups();
document.getElementById('group-comment').value = '';
}
// Сброс фильтров
function resetFilters() {
appState.filters = { category: 'all', status: 'all', period: 'all' };
document.getElementById('filter-category').value = 'all';
document.getElementById('filter-status').value = 'all';
document.getElementById('filter-period').value = 'all';
saveState();
renderGroups();
}
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
loadState();
renderGroups();
document.getElementById('filter-category').addEventListener('change', function() {
appState.filters.category = this.value;
saveState();
renderGroups();
});
document.getElementById('filter-status').addEventListener('change', function() {
appState.filters.status = this.value;
saveState();
renderGroups();
});
document.getElementById('filter-period').addEventListener('change', function() {
appState.filters.period = this.value;
saveState();
renderGroups();
});
document.getElementById('btn-reset').addEventListener('click', resetFilters);
document.getElementById('btn-clear-storage').addEventListener('click', function() {
if (confirm('Очистить все сохранённые данные и загрузить демо-данные заново?')) {
localStorage.removeItem('zakupki-review-state');
appState.groups = JSON.parse(JSON.stringify(DEMO_DATA));
appState.selectedItems.clear();
appState.filters = { category: 'all', status: 'all', period: 'all' };
document.getElementById('filter-category').value = 'all';
document.getElementById('filter-status').value = 'all';
document.getElementById('filter-period').value = 'all';
renderGroups();
}
});
// Первичная инициализация обработчиков кнопок
updateSelectionPanel();
});
</script>
</body>
</html>