Compare commits
No commits in common. "pages" and "main" have entirely different histories.
497
index.html
497
index.html
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Сравнение закупочного плана</title>
|
||||
<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">
|
||||
@ -23,30 +23,10 @@
|
||||
.kt-ai-table tr:hover td { background: var(--kt-ai-bg-soft); }
|
||||
.kt-ai-table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.kt-ai-table .actions { white-space: nowrap; }
|
||||
.kt-ai-table tr.selected { background: var(--kt-ai-bg-soft); }
|
||||
.kt-ai-table tr.excluded { opacity: 0.5; background: var(--kt-ai-bg-soft); }
|
||||
.kt-ai-table tr.excluded td { text-decoration: line-through; }
|
||||
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-4); color: var(--kt-ai-fg-muted); }
|
||||
.form-section { padding: var(--kt-ai-space-6) 0; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); max-width: 900px; }
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); max-width: 800px; }
|
||||
.filter-note { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-left: var(--kt-ai-space-2); }
|
||||
.compare-panel { display: none; padding: var(--kt-ai-space-6) 0; }
|
||||
.compare-panel.active { display: block; }
|
||||
.compare-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-4); }
|
||||
.compare-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||||
.compare-card h4 { margin: 0 0 var(--kt-ai-space-3) 0; font-size: var(--kt-ai-text-lg); }
|
||||
.compare-row { display: flex; justify-content: space-between; padding: var(--kt-ai-space-2) 0; border-bottom: 1px solid var(--kt-ai-border-strong); }
|
||||
.compare-row:last-child { border-bottom: none; }
|
||||
.compare-label { color: var(--kt-ai-fg-muted); }
|
||||
.compare-value { font-weight: var(--kt-ai-weight-medium); }
|
||||
.diff-highlight { background: var(--kt-ai-bg-soft); padding: var(--kt-ai-space-1) var(--kt-ai-space-2); border-radius: var(--kt-ai-radius-md); }
|
||||
.decision-section { padding: var(--kt-ai-space-6) 0; border-top: 1px solid var(--kt-ai-border); margin-top: var(--kt-ai-space-6); }
|
||||
.status-badge { display: inline-block; padding: var(--kt-ai-space-1) var(--kt-ai-space-3); border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
|
||||
.status-new { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||||
.status-review { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); }
|
||||
.status-approved { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||||
.status-rejected { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); }
|
||||
.toolbar { display: flex; gap: var(--kt-ai-space-3); align-items: center; margin-bottom: var(--kt-ai-space-4); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="kt-ai-app">
|
||||
@ -54,10 +34,9 @@
|
||||
<div class="kt-ai-wrap">
|
||||
<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="app-title">Учёт кабельной продукции</span>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
|
||||
<span class="status-badge status-new" id="header-status">Новые позиции</span>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
@ -67,22 +46,22 @@
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-total">0</div>
|
||||
<div class="label">Всего</div>
|
||||
<div class="hint">позиций в плане</div>
|
||||
<div class="hint">кабельных позиций</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-groups">0</div>
|
||||
<div class="label">Группы</div>
|
||||
<div class="hint">похожих товаров</div>
|
||||
<div class="value" id="kpi-in-work">0</div>
|
||||
<div class="label">В работе</div>
|
||||
<div class="hint">активных задач</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-excluded">0</div>
|
||||
<div class="label">Исключено</div>
|
||||
<div class="hint">строк отклонено</div>
|
||||
<div class="value" id="kpi-ready">0</div>
|
||||
<div class="label">Готово</div>
|
||||
<div class="hint">завершено</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-sum">0</div>
|
||||
<div class="label">Сумма, ₸</div>
|
||||
<div class="hint">общая стоимость</div>
|
||||
<div class="value" id="kpi-meters">0</div>
|
||||
<div class="label">Метров</div>
|
||||
<div class="hint">общая длина</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -91,13 +70,13 @@
|
||||
<section class="filters-section">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-field" style="min-width: 180px;">
|
||||
<label>Категория</label>
|
||||
<select class="kt-ai-select" id="filter-category">
|
||||
<option value="">Все категории</option>
|
||||
<option value="Кабель силовой">Кабель силовой</option>
|
||||
<option value="Кабель оптоволоконный">Кабель оптоволоконный</option>
|
||||
<option value="Оборудование">Оборудование</option>
|
||||
<option value="Материалы">Материалы</option>
|
||||
<label>Тип кабеля</label>
|
||||
<select class="kt-ai-select" id="filter-type">
|
||||
<option value="">Все типы</option>
|
||||
<option value="Силовой">Силовой</option>
|
||||
<option value="Оптоволоконный">Оптоволоконный</option>
|
||||
<option value="Коаксиальный">Коаксиальный</option>
|
||||
<option value="Контрольный">Контрольный</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field" style="min-width: 160px;">
|
||||
@ -105,51 +84,37 @@
|
||||
<select class="kt-ai-select" id="filter-status">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="new">Новый</option>
|
||||
<option value="review">На согласовании</option>
|
||||
<option value="approved">Утверждено</option>
|
||||
<option value="rejected">Отклонено</option>
|
||||
<option value="in-work">В работе</option>
|
||||
<option value="ready">Готово</option>
|
||||
<option value="archived">Архив</option>
|
||||
</select>
|
||||
<span class="filter-note" data-kt-filter-note>в демо не меняет KPI</span>
|
||||
</div>
|
||||
<div class="kt-ai-field" style="min-width: 200px;">
|
||||
<label>Период</label>
|
||||
<select class="kt-ai-select" id="filter-period">
|
||||
<option value="all">За всё время</option>
|
||||
<option value="2026-09">Сентябрь 2026</option>
|
||||
<option value="2026-10">Октябрь 2026</option>
|
||||
<option value="2026-Q4">4 квартал 2026</option>
|
||||
</select>
|
||||
<label>Поиск</label>
|
||||
<input class="kt-ai-input" id="filter-search" placeholder="Название или сечение...">
|
||||
</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="primary" id="btn-compare" disabled>Сравнить выбранные</button>
|
||||
<button class="kt-ai-btn" data-variant="ghost" id="btn-export">Выгрузить Excel</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-section">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="toolbar">
|
||||
<span style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Выделите 2-4 похожих строки для сравнения (Ctrl+клик)</span>
|
||||
</div>
|
||||
<div class="kt-ai-table-wrap">
|
||||
<table class="kt-ai-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;">☐</th>
|
||||
<th>Наименование</th>
|
||||
<th>Категория</th>
|
||||
<th class="num">Кол-во</th>
|
||||
<th class="num">Ед. изм.</th>
|
||||
<th class="num">Цена, ₸</th>
|
||||
<th class="num">Сумма, ₸</th>
|
||||
<th>Срок поставки</th>
|
||||
<th>Поставщик</th>
|
||||
<th>Название</th>
|
||||
<th>Тип</th>
|
||||
<th class="num">Сечение, мм²</th>
|
||||
<th class="num">Длина, м</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
<th class="actions">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="plan-table-body"></tbody>
|
||||
<tbody id="cable-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="kt-ai-empty" id="empty-state" style="display: none;">
|
||||
@ -161,164 +126,106 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="compare-panel" id="compare-panel">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Сравнение позиций</h2>
|
||||
<p>Выберите оптимальный вариант или исключите неподходящие</p>
|
||||
</div>
|
||||
<div class="compare-grid" id="compare-cards"></div>
|
||||
|
||||
<div class="decision-section">
|
||||
<h3 style="margin-bottom: var(--kt-ai-space-4);">Решение по группе</h3>
|
||||
<form id="decision-form" class="form-grid">
|
||||
<div class="kt-ai-field" style="grid-column: 1 / -1;">
|
||||
<label>Комментарий к решению *</label>
|
||||
<textarea class="kt-ai-textarea" name="comment" required rows="3" placeholder="Обоснуйте выбор или причину исключения..."></textarea>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Решение</label>
|
||||
<select class="kt-ai-select" name="decision" required>
|
||||
<option value="">Выберите решение</option>
|
||||
<option value="approve-best">Утвердить лучший вариант</option>
|
||||
<option value="exclude-all">Исключить все позиции</option>
|
||||
<option value="send-review">Отправить на доработку</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Ответственный</label>
|
||||
<input class="kt-ai-input" name="owner" required placeholder="ФИО">
|
||||
</div>
|
||||
<div style="grid-column: 1 / -1; display: flex; gap: var(--kt-ai-space-3);">
|
||||
<button class="kt-ai-btn" data-variant="primary" type="submit">Сохранить решение</button>
|
||||
<button class="kt-ai-btn" data-variant="ghost" type="button" id="btn-close-compare">Закрыть</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<div class="kt-ai-wrap">
|
||||
<div class="kt-ai-section-head">
|
||||
<h2>Добавить позицию в план</h2>
|
||||
<p>Заполните форму для создания новой строки закупочного плана</p>
|
||||
<h2>Добавить кабель</h2>
|
||||
<p>Заполните форму для создания новой записи</p>
|
||||
</div>
|
||||
<form id="add-plan-form" class="form-grid">
|
||||
<form id="add-cable-form" class="form-grid">
|
||||
<div class="kt-ai-field">
|
||||
<label>Наименование *</label>
|
||||
<input class="kt-ai-input" name="name" required placeholder="Например: Кабель АВВГ 3х2.5">
|
||||
<label>Название *</label>
|
||||
<input class="kt-ai-input" name="name" required placeholder="Например: АВВГ 3х2.5">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Категория *</label>
|
||||
<select class="kt-ai-select" name="category" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
<option value="Кабель силовой">Кабель силовой</option>
|
||||
<option value="Кабель оптоволоконный">Кабель оптоволоконный</option>
|
||||
<option value="Оборудование">Оборудование</option>
|
||||
<option value="Материалы">Материалы</option>
|
||||
<label>Тип *</label>
|
||||
<select class="kt-ai-select" name="type" required>
|
||||
<option value="">Выберите тип</option>
|
||||
<option value="Силовой">Силовой</option>
|
||||
<option value="Оптоволоконный">Оптоволоконный</option>
|
||||
<option value="Коаксиальный">Коаксиальный</option>
|
||||
<option value="Контрольный">Контрольный</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Количество *</label>
|
||||
<input class="kt-ai-input" name="quantity" type="number" required placeholder="100">
|
||||
<label>Сечение, мм² *</label>
|
||||
<input class="kt-ai-input" name="section" type="number" step="0.1" required placeholder="2.5">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Ед. изм.</label>
|
||||
<select class="kt-ai-select" name="unit">
|
||||
<option value="м">метры</option>
|
||||
<option value="шт">штуки</option>
|
||||
<option value="кг">килограммы</option>
|
||||
<option value="комп">комплекты</option>
|
||||
</select>
|
||||
<label>Длина, м *</label>
|
||||
<input class="kt-ai-input" name="length" type="number" required placeholder="100">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Цена за ед., ₸ *</label>
|
||||
<input class="kt-ai-input" name="price" type="number" required placeholder="500">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Срок поставки *</label>
|
||||
<input class="kt-ai-input" name="delivery" type="date" required>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Поставщик *</label>
|
||||
<input class="kt-ai-input" name="supplier" required placeholder="ООО «Пример»">
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Статус</label>
|
||||
<select class="kt-ai-select" name="status">
|
||||
<label>Статус *</label>
|
||||
<select class="kt-ai-select" name="status" required>
|
||||
<option value="new">Новый</option>
|
||||
<option value="review">На согласовании</option>
|
||||
<option value="in-work">В работе</option>
|
||||
<option value="ready">Готово</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-field">
|
||||
<label>Дата *</label>
|
||||
<input class="kt-ai-input" name="date" type="date" required>
|
||||
</div>
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<button class="kt-ai-btn" data-variant="primary" type="submit">Добавить в план</button>
|
||||
<button class="kt-ai-btn" data-variant="primary" type="submit">Добавить в учёт</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="kt-ai-wrap kt-ai-pagefoot">
|
||||
<span>© 2026 Казахтелеком — Сравнение закупочного плана</span>
|
||||
<span>© 2026 Казахтелеком — Учёт кабельной продукции</span>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<span>Демо-прототип</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STORAGE_KEY = 'kt_procurement_data';
|
||||
const STORAGE_KEY = 'kt_cables_data';
|
||||
|
||||
const DEFAULT_PLAN = [
|
||||
{ id: 1, name: 'Кабель АВВГ 3х2.5', category: 'Кабель силовой', quantity: 150, unit: 'м', price: 450, sum: 67500, delivery: '2026-09-20', supplier: 'ТОО «КабельКЗ»', status: 'new', period: '2026-09' },
|
||||
{ id: 2, name: 'Кабель АВВГ 3х2.5', category: 'Кабель силовой', quantity: 200, unit: 'м', price: 420, sum: 84000, delivery: '2026-09-25', supplier: 'ИП «Алмаз»', status: 'review', period: '2026-09' },
|
||||
{ id: 3, name: 'Кабель АВВГ 3х2.5', category: 'Кабель силовой', quantity: 100, unit: 'м', price: 480, sum: 48000, delivery: '2026-09-15', supplier: 'ТОО «ЭлектроМир»', status: 'new', period: '2026-09' },
|
||||
{ id: 4, name: 'Кабель АВВГ 3х4.0', category: 'Кабель силовой', quantity: 180, unit: 'м', price: 680, sum: 122400, delivery: '2026-09-22', supplier: 'ТОО «КабельКЗ»', status: 'new', period: '2026-09' },
|
||||
{ id: 5, name: 'Кабель ОКЛ-Т 4х9', category: 'Кабель оптоволоконный', quantity: 500, unit: 'м', price: 1250, sum: 625000, delivery: '2026-10-10', supplier: 'ТОО «Оптима»', status: 'review', period: '2026-10' },
|
||||
{ id: 6, name: 'Кабель ОКЛ-Т 4х9', category: 'Кабель оптоволоконный', quantity: 450, unit: 'м', price: 1180, sum: 531000, delivery: '2026-10-05', supplier: 'АО «ТелекомСнаб»', status: 'new', period: '2026-10' },
|
||||
{ id: 7, name: 'Коммутатор Cisco 2960', category: 'Оборудование', quantity: 5, unit: 'шт', price: 185000, sum: 925000, delivery: '2026-10-15', supplier: 'ТОО «IT-Дистрибьюция»', status: 'approved', period: '2026-10' },
|
||||
{ id: 8, name: 'Кронштейн универсальный', category: 'Материалы', quantity: 50, unit: 'шт', price: 2500, sum: 125000, delivery: '2026-09-18', supplier: 'ИП «СтройМетиз»', status: 'new', period: '2026-09' },
|
||||
{ id: 9, name: 'Кронштейн универсальный', category: 'Материалы', quantity: 60, unit: 'шт', price: 2300, sum: 138000, delivery: '2026-09-28', supplier: 'ТОО «КрепежЦентр»', status: 'review', period: '2026-09' }
|
||||
const DEFAULT_CABLES = [
|
||||
{ id: 1, name: 'АВВГ 3х2.5', type: 'Силовой', section: 2.5, length: 150, status: 'in-work', date: '2026-09-10' },
|
||||
{ id: 2, name: 'АВВГ 3х4.0', type: 'Силовой', section: 4.0, length: 200, status: 'new', date: '2026-09-12' },
|
||||
{ id: 3, name: 'АВВГ 3х2.5', type: 'Силовой', section: 2.5, length: 80, status: 'ready', date: '2026-09-08' },
|
||||
{ id: 4, name: 'ОКЛ-Т 4х9', type: 'Оптоволоконный', section: 9, length: 500, status: 'in-work', date: '2026-09-11' },
|
||||
{ id: 5, name: 'КВВГ 7х1.5', type: 'Контрольный', section: 1.5, length: 120, status: 'archived', date: '2026-09-05' }
|
||||
];
|
||||
|
||||
let selectedIds = [];
|
||||
let excludedIds = [];
|
||||
let decisions = [];
|
||||
|
||||
function getData() {
|
||||
function getCables() {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return parsed.planItems || DEFAULT_PLAN;
|
||||
return parsed.userCables || [];
|
||||
}
|
||||
return DEFAULT_PLAN;
|
||||
return DEFAULT_CABLES;
|
||||
}
|
||||
|
||||
function savePlanItem(item) {
|
||||
function saveCable(cable) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
let data = stored ? JSON.parse(stored) : { planItems: [], decisions: [] };
|
||||
if (!data.planItems) data.planItems = [];
|
||||
item.id = Date.now();
|
||||
item.sum = (item.quantity * item.price);
|
||||
data.planItems.push(item);
|
||||
let data = stored ? JSON.parse(stored) : { userCables: [] };
|
||||
if (!data.userCables) data.userCables = [];
|
||||
cable.id = Date.now();
|
||||
data.userCables.push(cable);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
function saveDecision(decision) {
|
||||
function deleteCable(id) {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
let data = stored ? JSON.parse(stored) : { planItems: [], decisions: [] };
|
||||
if (!data.decisions) data.decisions = [];
|
||||
decision.id = Date.now();
|
||||
decision.date = new Date().toISOString();
|
||||
data.decisions.push(decision);
|
||||
if (stored) {
|
||||
let data = JSON.parse(stored);
|
||||
if (data.userCables) {
|
||||
data.userCables = data.userCables.filter(c => c.id !== id);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
decisions = data.decisions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusChip(status) {
|
||||
const map = {
|
||||
'new': { label: 'Новый', tone: 'blue' },
|
||||
'review': { label: 'На согласовании', tone: 'orange' },
|
||||
'approved': { label: 'Утверждено', tone: 'green' },
|
||||
'rejected': { label: 'Отклонено', tone: 'red' }
|
||||
'in-work': { label: 'В работе', tone: 'orange' },
|
||||
'ready': { label: 'Готово', tone: 'green' },
|
||||
'archived': { label: 'Архив', tone: 'gray' }
|
||||
};
|
||||
const s = map[status] || { label: status, tone: 'gray' };
|
||||
return `<span class="kt-ai-chip" data-tone="${s.tone}">${s.label}</span>`;
|
||||
@ -329,239 +236,85 @@ function formatDate(dateStr) {
|
||||
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||
}
|
||||
|
||||
function formatMoney(amount) {
|
||||
return new Intl.NumberFormat('ru-RU').format(amount);
|
||||
function updateKPI(cables) {
|
||||
document.getElementById('kpi-total').textContent = cables.length;
|
||||
document.getElementById('kpi-in-work').textContent = cables.filter(c => c.status === 'in-work').length;
|
||||
document.getElementById('kpi-ready').textContent = cables.filter(c => c.status === 'ready').length;
|
||||
document.getElementById('kpi-meters').textContent = cables.reduce((sum, c) => sum + (parseInt(c.length) || 0), 0);
|
||||
}
|
||||
|
||||
function updateKPI(items) {
|
||||
document.getElementById('kpi-total').textContent = items.length;
|
||||
const groups = new Set(items.map(i => i.name)).size;
|
||||
document.getElementById('kpi-groups').textContent = groups;
|
||||
document.getElementById('kpi-excluded').textContent = excludedIds.length;
|
||||
document.getElementById('kpi-sum').textContent = formatMoney(items.reduce((sum, i) => sum + (i.sum || 0), 0));
|
||||
}
|
||||
|
||||
function renderTable(items) {
|
||||
const tbody = document.getElementById('plan-table-body');
|
||||
function renderTable(cables) {
|
||||
const tbody = document.getElementById('cable-table-body');
|
||||
const empty = document.getElementById('empty-state');
|
||||
|
||||
if (items.length === 0) {
|
||||
if (cables.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
empty.style.display = 'none';
|
||||
tbody.innerHTML = items.map(item => {
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
const isExcluded = excludedIds.includes(item.id);
|
||||
const rowClass = [
|
||||
isSelected ? 'selected' : '',
|
||||
isExcluded ? 'excluded' : ''
|
||||
].filter(Boolean).join(' ') || '';
|
||||
|
||||
return `
|
||||
<tr class="${rowClass}" data-id="${item.id}">
|
||||
<td>
|
||||
<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleSelect(${item.id})">
|
||||
</td>
|
||||
<td>${item.name}</td>
|
||||
<td>${item.category}</td>
|
||||
<td class="num">${item.quantity}</td>
|
||||
<td class="num">${item.unit}</td>
|
||||
<td class="num">${formatMoney(item.price)}</td>
|
||||
<td class="num">${formatMoney(item.sum)}</td>
|
||||
<td>${formatDate(item.delivery)}</td>
|
||||
<td>${item.supplier}</td>
|
||||
<td>${getStatusChip(item.status)}</td>
|
||||
tbody.innerHTML = cables.map(c => `
|
||||
<tr>
|
||||
<td>${c.name}</td>
|
||||
<td>${c.type}</td>
|
||||
<td class="num">${c.section}</td>
|
||||
<td class="num">${c.length}</td>
|
||||
<td>${getStatusChip(c.status)}</td>
|
||||
<td>${formatDate(c.date)}</td>
|
||||
<td class="actions">
|
||||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="excludeItem(${item.id})" ${isExcluded ? 'disabled' : ''}>
|
||||
${isExcluded ? 'Исключено' : 'Исключить'}
|
||||
</button>
|
||||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="deleteRow(${c.id})">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
`}).join('');
|
||||
|
||||
const compareBtn = document.getElementById('btn-compare');
|
||||
compareBtn.disabled = selectedIds.length < 2;
|
||||
compareBtn.textContent = selectedIds.length > 0
|
||||
? `Сравнить выбранные (${selectedIds.length})`
|
||||
: 'Сравнить выбранные';
|
||||
`).join('');
|
||||
}
|
||||
|
||||
window.toggleSelect = function(id) {
|
||||
const idx = selectedIds.indexOf(id);
|
||||
if (idx > -1) {
|
||||
selectedIds.splice(idx, 1);
|
||||
} else {
|
||||
if (selectedIds.length >= 4) {
|
||||
alert('Можно выбрать не более 4 позиций для сравнения');
|
||||
return;
|
||||
}
|
||||
selectedIds.push(id);
|
||||
}
|
||||
applyFilters();
|
||||
};
|
||||
|
||||
window.excludeItem = function(id) {
|
||||
const reason = prompt('Укажите причину исключения:');
|
||||
if (reason) {
|
||||
excludedIds.push(id);
|
||||
applyFilters();
|
||||
}
|
||||
};
|
||||
|
||||
function applyFilters() {
|
||||
const categoryFilter = document.getElementById('filter-category').value;
|
||||
const typeFilter = document.getElementById('filter-type').value;
|
||||
const statusFilter = document.getElementById('filter-status').value;
|
||||
const periodFilter = document.getElementById('filter-period').value;
|
||||
const searchFilter = document.getElementById('filter-search').value.toLowerCase();
|
||||
|
||||
let items = getData();
|
||||
let cables = getCables();
|
||||
|
||||
if (categoryFilter) items = items.filter(i => i.category === categoryFilter);
|
||||
if (statusFilter) items = items.filter(i => i.status === statusFilter);
|
||||
if (periodFilter && periodFilter !== 'all') {
|
||||
if (periodFilter.includes('Q')) {
|
||||
const quarter = periodFilter.split('-Q')[1];
|
||||
const year = periodFilter.split('-Q')[0];
|
||||
const qMonths = { '4': ['10', '11', '12'] }[quarter];
|
||||
items = items.filter(i => qMonths.some(m => i.period.startsWith(year + '-' + m)));
|
||||
} else {
|
||||
items = items.filter(i => i.period === periodFilter);
|
||||
}
|
||||
if (typeFilter) cables = cables.filter(c => c.type === typeFilter);
|
||||
if (statusFilter) cables = cables.filter(c => c.status === statusFilter);
|
||||
if (searchFilter) cables = cables.filter(c =>
|
||||
c.name.toLowerCase().includes(searchFilter) ||
|
||||
String(c.section).includes(searchFilter)
|
||||
);
|
||||
|
||||
updateKPI(cables);
|
||||
renderTable(cables);
|
||||
}
|
||||
|
||||
updateKPI(items);
|
||||
renderTable(items);
|
||||
}
|
||||
|
||||
function showComparePanel() {
|
||||
if (selectedIds.length < 2) {
|
||||
alert('Выберите минимум 2 позиции для сравнения');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = getData().filter(i => selectedIds.includes(i.id));
|
||||
const panel = document.getElementById('compare-panel');
|
||||
const cardsContainer = document.getElementById('compare-cards');
|
||||
|
||||
cardsContainer.innerHTML = items.map((item, idx) => `
|
||||
<div class="compare-card">
|
||||
<h4>${item.name} #${idx + 1}</h4>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Поставщик</span>
|
||||
<span class="compare-value">${item.supplier}</span>
|
||||
</div>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Цена за ед.</span>
|
||||
<span class="compare-value">${formatMoney(item.price)} ₸</span>
|
||||
</div>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Количество</span>
|
||||
<span class="compare-value">${item.quantity} ${item.unit}</span>
|
||||
</div>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Сумма</span>
|
||||
<span class="compare-value">${formatMoney(item.sum)} ₸</span>
|
||||
</div>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Срок поставки</span>
|
||||
<span class="compare-value ${idx === 0 ? 'diff-highlight' : ''}">${formatDate(item.delivery)}</span>
|
||||
</div>
|
||||
<div class="compare-row">
|
||||
<span class="compare-label">Статус</span>
|
||||
<span class="compare-value">${getStatusChip(item.status)}</span>
|
||||
</div>
|
||||
<div style="margin-top: var(--kt-ai-space-3);">
|
||||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" style="width: 100%;" onclick="excludeFromCompare(${item.id})">
|
||||
Исключить эту позицию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
panel.classList.add('active');
|
||||
panel.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
window.excludeFromCompare = function(id) {
|
||||
const reason = prompt('Укажите причину исключения:');
|
||||
if (reason) {
|
||||
excludedIds.push(id);
|
||||
selectedIds = selectedIds.filter(sid => sid !== id);
|
||||
function deleteRow(id) {
|
||||
deleteCable(id);
|
||||
applyFilters();
|
||||
document.getElementById('compare-panel').classList.remove('active');
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('btn-compare').addEventListener('click', showComparePanel);
|
||||
|
||||
document.getElementById('btn-close-compare').addEventListener('click', () => {
|
||||
document.getElementById('compare-panel').classList.remove('active');
|
||||
});
|
||||
|
||||
document.getElementById('decision-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const decision = {
|
||||
selectedIds: [...selectedIds],
|
||||
excludedIds: [...excludedIds],
|
||||
comment: form.comment.value,
|
||||
decision: form.decision.value,
|
||||
owner: form.owner.value
|
||||
};
|
||||
saveDecision(decision);
|
||||
alert('Решение сохранено!');
|
||||
form.reset();
|
||||
document.getElementById('compare-panel').classList.remove('active');
|
||||
selectedIds = [];
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('btn-export').addEventListener('click', () => {
|
||||
const items = getData();
|
||||
const csvContent = [
|
||||
['ID', 'Наименование', 'Категория', 'Количество', 'Ед. изм.', 'Цена', 'Сумма', 'Срок поставки', 'Поставщик', 'Статус', 'Исключено'],
|
||||
...items.map(i => [
|
||||
i.id, i.name, i.category, i.quantity, i.unit, i.price, i.sum, i.delivery, i.supplier, i.status,
|
||||
excludedIds.includes(i.id) ? 'Да' : 'Нет'
|
||||
])
|
||||
].map(row => row.join(';')).join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = 'закупочный_план_' + new Date().toISOString().slice(0, 10) + '.csv';
|
||||
link.click();
|
||||
});
|
||||
|
||||
document.getElementById('filter-category').addEventListener('change', applyFilters);
|
||||
document.getElementById('filter-type').addEventListener('change', applyFilters);
|
||||
document.getElementById('filter-status').addEventListener('change', applyFilters);
|
||||
document.getElementById('filter-period').addEventListener('change', applyFilters);
|
||||
document.getElementById('filter-search').addEventListener('input', applyFilters);
|
||||
|
||||
document.getElementById('btn-reset').addEventListener('click', () => {
|
||||
document.getElementById('filter-category').value = '';
|
||||
document.getElementById('filter-type').value = '';
|
||||
document.getElementById('filter-status').value = '';
|
||||
document.getElementById('filter-period').value = 'all';
|
||||
selectedIds = [];
|
||||
document.getElementById('filter-search').value = '';
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('add-plan-form').addEventListener('submit', (e) => {
|
||||
document.getElementById('add-cable-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const item = {
|
||||
const cable = {
|
||||
name: form.name.value,
|
||||
category: form.category.value,
|
||||
quantity: parseInt(form.quantity.value),
|
||||
unit: form.unit.value,
|
||||
price: parseInt(form.price.value),
|
||||
delivery: form.delivery.value,
|
||||
supplier: form.supplier.value,
|
||||
type: form.type.value,
|
||||
section: parseFloat(form.section.value),
|
||||
length: parseInt(form.length.value),
|
||||
status: form.status.value,
|
||||
period: form.delivery.value.slice(0, 7)
|
||||
date: form.date.value
|
||||
};
|
||||
savePlanItem(item);
|
||||
saveCable(cable);
|
||||
form.reset();
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user