ya-daniyar-zanimayus-zakupka/index.html

514 lines
24 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.kt-ai-app { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); margin: 0; }
.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-lg); font-weight: var(--kt-ai-weight-semibold); }
.filters-bar { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-5); }
.filter-group { display: flex; align-items: center; gap: var(--kt-ai-space-2); }
.filter-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.kt-ai-table-wrap { overflow-x: auto; border: 1px solid var(--kt-ai-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); background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg-muted); }
.kt-ai-table td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); vertical-align: middle; }
.kt-ai-table tr:hover { background: var(--kt-ai-bg-soft); }
.kt-ai-table tr.selected { background: rgba(0, 122, 255, 0.08); }
.kt-ai-table tr.disabled { opacity: 0.5; pointer-events: none; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.actions-cell { display: flex; gap: var(--kt-ai-space-2); }
.comment-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.comment-modal.open { display: flex; }
.comment-box { background: var(--kt-ai-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); width: 100%; max-width: 480px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); }
.comment-box h3 { margin: 0 0 var(--kt-ai-space-4); font-size: var(--kt-ai-text-lg); }
.comment-box textarea { width: 100%; min-height: 100px; margin-bottom: var(--kt-ai-space-4); }
.comment-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; }
.stats-strip { display: flex; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-5); flex-wrap: wrap; }
.stat-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); min-width: 160px; }
.stat-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
.stat-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-1); }
.empty-state { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
.bulk-actions { display: flex; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-4); padding: var(--kt-ai-space-3); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); }
.selected-count { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.compare-panel { position: fixed; bottom: 0; left: 0; right: 0; background: var(--kt-ai-bg); border-top: 1px solid var(--kt-ai-border); padding: var(--kt-ai-space-4); display: none; z-index: 100; }
.compare-panel.open { display: block; }
.compare-list { display: flex; gap: var(--kt-ai-space-3); overflow-x: auto; padding-bottom: var(--kt-ai-space-3); }
.compare-item { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-lg); padding: var(--kt-ai-space-3); min-width: 200px; position: relative; }
.compare-item .remove { position: absolute; top: var(--kt-ai-space-1); right: var(--kt-ai-space-2); cursor: pointer; color: var(--kt-ai-fg-muted); }
.compare-item .name { font-weight: var(--kt-ai-weight-medium); font-size: var(--kt-ai-text-sm); margin-bottom: var(--kt-ai-space-2); }
.compare-item .detail { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.compare-actions { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-3); }
.highlight-group { background: rgba(255, 191, 0, 0.1); }
</style>
</head>
<body class="kt-ai-app">
<div class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-4);padding-bottom:var(--kt-ai-space-4)">
<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-chip" data-tone="orange">Демо-данные</span>
<span class="kt-ai-spacer"></span>
<span style="font-size:var(--kt-ai-text-sm);color:var(--kt-ai-fg-muted)">Данияр</span>
</header>
<div class="stats-strip" style="margin-top:var(--kt-ai-space-5)">
<div class="stat-card">
<div class="stat-value" id="stat-total">0</div>
<div class="stat-label">Всего позиций</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-new">0</div>
<div class="stat-label">Новые</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-confirmed">0</div>
<div class="stat-label">Подтверждено</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-rejected">0</div>
<div class="stat-label">Отклонено</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-amount">0 ₸</div>
<div class="stat-label">Сумма</div>
</div>
</div>
<div class="filters-bar">
<div class="filter-group">
<label class="filter-label">Поиск:</label>
<input class="kt-ai-input" id="filter-search" placeholder="Название, код..." style="width:220px">
</div>
<div class="filter-group">
<label class="filter-label">Категория:</label>
<select class="kt-ai-select" id="filter-category">
<option value="">Все</option>
<option>IT-оборудование</option>
<option>Канцтовары</option>
<option>Мебель</option>
<option>Расходные материалы</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Статус:</label>
<select class="kt-ai-select" id="filter-status">
<option value="">Все</option>
<option value="new">Новые</option>
<option value="confirmed">Подтверждено</option>
<option value="rejected">Отклонено</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Группа:</label>
<select class="kt-ai-select" id="filter-group">
<option value="">Все</option>
<option value="A">Группа A</option>
<option value="B">Группа B</option>
<option value="C">Группа C</option>
</select>
</div>
<button class="kt-ai-btn" data-variant="ghost" id="btn-reset-filters">Сбросить</button>
</div>
<div class="bulk-actions">
<span class="selected-count" id="selected-count">Выбрано: 0</span>
<button class="kt-ai-btn" data-variant="primary" id="btn-bulk-confirm" disabled>Подтвердить выбранные</button>
<button class="kt-ai-btn" data-variant="danger" id="btn-bulk-reject" disabled>Отклонить выбранные</button>
<button class="kt-ai-btn" data-variant="ghost" id="btn-compare">Сравнить выбранные</button>
</div>
<div class="kt-ai-table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th style="width:40px"><input type="checkbox" id="check-all"></th>
<th>Код</th>
<th>Наименование</th>
<th>Категория</th>
<th>Группа</th>
<th class="num">Количество</th>
<th class="num">Цена (₸)</th>
<th class="num">Сумма (₸)</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<div class="empty-state" id="empty-state" style="display:none">
<p style="font-size:var(--kt-ai-text-lg);margin-bottom:var(--kt-ai-space-2)">Нет позиций для отображения</p>
<p style="font-size:var(--kt-ai-text-sm)">Измените параметры фильтров или добавьте новые позиции</p>
</div>
</div>
<div class="compare-panel" id="compare-panel">
<div class="kt-ai-wrap">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--kt-ai-space-3)">
<strong>Сравнение позиций</strong>
<button class="kt-ai-btn" data-variant="ghost" id="btn-close-compare">Закрыть</button>
</div>
<div class="compare-list" id="compare-list"></div>
<div class="compare-actions">
<button class="kt-ai-btn" data-variant="primary" id="btn-confirm-compare">Подтвердить группу</button>
<button class="kt-ai-btn" data-variant="danger" id="btn-reject-compare">Отклонить группу</button>
</div>
</div>
</div>
<div class="comment-modal" id="comment-modal">
<div class="comment-box">
<h3 id="comment-title">Комментарий</h3>
<textarea class="kt-ai-textarea" id="comment-text" placeholder="Введите комментарий..."></textarea>
<div class="comment-actions">
<button class="kt-ai-btn" data-variant="ghost" id="btn-cancel-comment">Отмена</button>
<button class="kt-ai-btn" data-variant="primary" id="btn-save-comment">Сохранить</button>
</div>
</div>
</div>
<script>
// Демо-данные для годового плана
const DEMO_DATA = [
{ id: 1, code: "ZK-2026-001", name: "Ноутбук Dell Latitude 3520", category: "IT-оборудование", group: "A", qty: 15, price: 285000, status: "new", comment: "" },
{ id: 2, code: "ZK-2026-002", name: "Ноутбук HP ProBook 450", category: "IT-оборудование", group: "A", qty: 10, price: 295000, status: "new", comment: "" },
{ id: 3, code: "ZK-2026-003", name: "Ноутбук Lenovo ThinkPad E15", category: "IT-оборудование", group: "A", qty: 12, price: 278000, status: "confirmed", comment: "Выбрали Dell, эта позиция лишняя" },
{ id: 4, code: "ZK-2026-004", name: "Бумага A4 SvetoCopy", category: "Канцтовары", group: "B", qty: 500, price: 1850, status: "new", comment: "" },
{ id: 5, code: "ZK-2026-005", name: "Бумага A4 Xerox Business", category: "Канцтовары", group: "B", qty: 300, price: 2100, status: "rejected", comment: "Дорого, есть аналог дешевле" },
{ id: 6, code: "ZK-2026-006", name: "Кресло офисное Chairman", category: "Мебель", group: "C", qty: 25, price: 45000, status: "new", comment: "" },
{ id: 7, code: "ZK-2026-007", name: "Кресло офисное Nowy Styl", category: "Мебель", group: "C", qty: 20, price: 52000, status: "new", comment: "" },
{ id: 8, code: "ZK-2026-008", name: "Стол офисный 1200мм", category: "Мебель", group: "C", qty: 30, price: 68000, status: "confirmed", comment: "" },
{ id: 9, code: "ZK-2026-009", name: "Картридж HP 12A", category: "Расходные материалы", group: "B", qty: 100, price: 8500, status: "new", comment: "" },
{ id: 10, code: "ZK-2026-010", name: "Картридж Canon 725", category: "Расходные материалы", group: "B", qty: 80, price: 7200, status: "new", comment: "" },
{ id: 11, code: "ZK-2026-011", name: "Монитор Dell 24 P2422H", category: "IT-оборудование", group: "A", qty: 20, price: 95000, status: "new", comment: "" },
{ id: 12, code: "ZK-2026-012", name: "Монитор LG 24MK600M", category: "IT-оборудование", group: "A", qty: 18, price: 88000, status: "rejected", comment: "Мало портов" },
{ id: 13, code: "ZK-2026-013", name: "Ручка шариковая синяя", category: "Канцтовары", group: "B", qty: 1000, price: 150, status: "confirmed", comment: "" },
{ id: 14, code: "ZK-2026-014", name: "Папка-регистратор А4", category: "Канцтовары", group: "B", qty: 200, price: 850, status: "new", comment: "" },
{ id: 15, code: "ZK-2026-015", name: "Степлер офисный", category: "Канцтовары", group: "B", qty: 50, price: 1200, status: "new", comment: "" }
];
let positions = [];
let selectedIds = new Set();
let compareIds = new Set();
let pendingAction = null;
let pendingIds = [];
// Загрузка данных из localStorage или демо
function loadData() {
const saved = localStorage.getItem('zakupki_positions');
if (saved) {
positions = JSON.parse(saved);
} else {
positions = [...DEMO_DATA];
saveData();
}
}
function saveData() {
localStorage.setItem('zakupki_positions', JSON.stringify(positions));
}
// Форматирование цены
function formatPrice(num) {
return num.toString().replace(/\s/g, '').replace(/(\d)(?=(\d{3})+$)/g, '$1 ');
}
// Отрисовка статистики
function renderStats() {
const total = positions.length;
const newCount = positions.filter(p => p.status === 'new').length;
const confirmed = positions.filter(p => p.status === 'confirmed').length;
const rejected = positions.filter(p => p.status === 'rejected').length;
const amount = positions.filter(p => p.status === 'confirmed').reduce((sum, p) => sum + p.qty * p.price, 0);
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-new').textContent = newCount;
document.getElementById('stat-confirmed').textContent = confirmed;
document.getElementById('stat-rejected').textContent = rejected;
document.getElementById('stat-amount').textContent = formatPrice(amount) + ' ₸';
}
// Получение статус-чипа
function getStatusChip(status) {
const config = {
new: { text: 'Новая', tone: 'blue' },
confirmed: { text: 'Подтверждено', tone: 'green' },
rejected: { text: 'Отклонено', tone: 'red' }
};
const c = config[status];
return `<span class="kt-ai-chip" data-tone="${c.tone}">${c.text}</span>`;
}
// Фильтрация данных
function getFilteredData() {
const search = document.getElementById('filter-search').value.toLowerCase();
const category = document.getElementById('filter-category').value;
const status = document.getElementById('filter-status').value;
const group = document.getElementById('filter-group').value;
return positions.filter(p => {
if (search && !p.name.toLowerCase().includes(search) && !p.code.toLowerCase().includes(search)) return false;
if (category && p.category !== category) return false;
if (status && p.status !== status) return false;
if (group && p.group !== group) return false;
return true;
});
}
// Отрисовка таблицы
function renderTable() {
const data = getFilteredData();
const tbody = document.getElementById('table-body');
const emptyState = document.getElementById('empty-state');
if (data.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
tbody.innerHTML = data.map(p => {
const sum = p.qty * p.price;
const isSelected = selectedIds.has(p.id);
const rowClass = isSelected ? 'selected' : '';
const isDisabled = p.status === 'confirmed' || p.status === 'rejected';
const disabledClass = isDisabled ? 'disabled' : '';
return `
<tr class="${rowClass} ${disabledClass}" data-id="${p.id}">
<td><input type="checkbox" class="row-checkbox" data-id="${p.id}" ${isSelected ? 'checked' : ''} ${isDisabled ? 'disabled' : ''}></td>
<td style="font-family:var(--kt-ai-font-mono);font-size:var(--kt-ai-text-xs)">${p.code}</td>
<td>${p.name}</td>
<td>${p.category}</td>
<td><span class="kt-ai-chip" data-tone="gray">Группа ${p.group}</span></td>
<td class="num">${formatPrice(p.qty)}</td>
<td class="num">${formatPrice(p.price)}</td>
<td class="num">${formatPrice(sum)}</td>
<td>${getStatusChip(p.status)}</td>
<td class="actions-cell">
${p.status === 'new' ? `
<button class="kt-ai-btn" data-variant="ghost" data-action="confirm" data-id="${p.id}">✓</button>
<button class="kt-ai-btn" data-variant="ghost" data-action="reject" data-id="${p.id}">✗</button>
` : `
<button class="kt-ai-btn" data-variant="ghost" data-action="view" data-id="${p.id}" title="Просмотр">👁</button>
`}
</td>
</tr>
`;
}).join('');
updateBulkActions();
}
// Обновление кнопок массовых действий
function updateBulkActions() {
const count = selectedIds.size;
document.getElementById('selected-count').textContent = `Выбрано: ${count}`;
document.getElementById('btn-bulk-confirm').disabled = count === 0;
document.getElementById('btn-bulk-reject').disabled = count === 0;
document.getElementById('btn-compare').disabled = count < 2;
}
// Открытие модального окна комментария
function openCommentModal(action, ids) {
pendingAction = action;
pendingIds = ids;
const modal = document.getElementById('comment-modal');
const title = document.getElementById('comment-title');
const text = document.getElementById('comment-text');
title.textContent = ids.length > 1
? `Комментарий для ${ids.length} позиций`
: (action === 'confirm' ? 'Подтверждение позиции' : 'Отклонение позиции');
text.value = '';
modal.classList.add('open');
text.focus();
}
// Закрытие модального окна
function closeCommentModal() {
document.getElementById('comment-modal').classList.remove('open');
pendingAction = null;
pendingIds = [];
}
// Применение действия
function applyAction(action, ids, comment) {
positions = positions.map(p => {
if (ids.includes(p.id)) {
return { ...p, status: action === 'confirm' ? 'confirmed' : 'rejected', comment };
}
return p;
});
saveData();
selectedIds.clear();
compareIds.clear();
document.getElementById('compare-panel').classList.remove('open');
renderStats();
renderTable();
}
// Обработчик чекбоксов
function handleCheckbox(id, checked) {
if (checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
compareIds.delete(id);
}
updateBulkActions();
renderComparePanel();
}
// Открытие панели сравнения
function openComparePanel() {
const panel = document.getElementById('compare-panel');
panel.classList.add('open');
renderComparePanel();
}
// Отрисовка панели сравнения
function renderComparePanel() {
const list = document.getElementById('compare-list');
const items = positions.filter(p => compareIds.has(p.id));
if (items.length === 0) {
list.innerHTML = '<p style="color:var(--kt-ai-fg-muted);padding:var(--kt-ai-space-3)">Выберите минимум 2 позиции для сравнения</p>';
return;
}
list.innerHTML = items.map(p => `
<div class="compare-item">
<span class="remove" data-action="remove-compare" data-id="${p.id}">✕</span>
<div class="name">${p.name}</div>
<div class="detail">Код: ${p.code}</div>
<div class="detail">Кол-во: ${formatPrice(p.qty)}</div>
<div class="detail">Цена: ${formatPrice(p.price)} ₸</div>
<div class="detail">Сумма: ${formatPrice(p.qty * p.price)} ₸</div>
</div>
`).join('');
}
// Инициализация
function init() {
loadData();
renderStats();
renderTable();
// Фильтры
document.getElementById('filter-search').addEventListener('input', renderTable);
document.getElementById('filter-category').addEventListener('change', renderTable);
document.getElementById('filter-status').addEventListener('change', renderTable);
document.getElementById('filter-group').addEventListener('change', renderTable);
document.getElementById('btn-reset-filters').addEventListener('click', () => {
document.getElementById('filter-search').value = '';
document.getElementById('filter-category').value = '';
document.getElementById('filter-status').value = '';
document.getElementById('filter-group').value = '';
renderTable();
});
// Чекбокс "выбрать всё"
document.getElementById('check-all').addEventListener('change', (e) => {
const filtered = getFilteredData();
const available = filtered.filter(p => p.status === 'new').map(p => p.id);
if (e.target.checked) {
available.forEach(id => selectedIds.add(id));
} else {
available.forEach(id => selectedIds.delete(id));
}
renderTable();
});
// Клик по таблице
document.getElementById('table-body').addEventListener('click', (e) => {
const checkbox = e.target.closest('.row-checkbox');
const btn = e.target.closest('button[data-action]');
if (checkbox) {
handleCheckbox(parseInt(checkbox.dataset.id), checkbox.checked);
return;
}
if (btn) {
const action = btn.dataset.action;
const id = parseInt(btn.dataset.id);
if (action === 'confirm') {
openCommentModal('confirm', [id]);
} else if (action === 'reject') {
openCommentModal('reject', [id]);
} else if (action === 'view') {
const pos = positions.find(p => p.id === id);
alert(`Позиция: ${pos.name}\nСтатус: ${pos.status}\nКомментарий: ${pos.comment || '—'}`);
}
}
});
// Массовые действия
document.getElementById('btn-bulk-confirm').addEventListener('click', () => {
openCommentModal('confirm', [...selectedIds]);
});
document.getElementById('btn-bulk-reject').addEventListener('click', () => {
openCommentModal('reject', [...selectedIds]);
});
document.getElementById('btn-compare').addEventListener('click', openComparePanel);
// Сравнение
document.getElementById('btn-close-compare').addEventListener('click', () => {
document.getElementById('compare-panel').classList.remove('open');
});
document.getElementById('compare-list').addEventListener('click', (e) => {
if (e.target.dataset.action === 'remove-compare') {
compareIds.delete(parseInt(e.target.dataset.id));
selectedIds.delete(parseInt(e.target.dataset.id));
renderComparePanel();
renderTable();
}
});
document.getElementById('btn-confirm-compare').addEventListener('click', () => {
if (compareIds.size === 0) return;
openCommentModal('confirm', [...compareIds]);
});
document.getElementById('btn-reject-compare').addEventListener('click', () => {
if (compareIds.size === 0) return;
openCommentModal('reject', [...compareIds]);
});
// Модальное окно
document.getElementById('btn-cancel-comment').addEventListener('click', closeCommentModal);
document.getElementById('btn-save-comment').addEventListener('click', () => {
const comment = document.getElementById('comment-text').value.trim();
if (pendingAction && pendingIds.length > 0) {
applyAction(pendingAction, pendingIds, comment);
closeCommentModal();
}
});
// Закрытие по клику вне
document.getElementById('comment-modal').addEventListener('click', (e) => {
if (e.target.id === 'comment-modal') {
closeCommentModal();
}
});
}
init();
</script>
</body>
</html>