Экран сравнения позиций годового плана закупок

This commit is contained in:
tore 2026-09-14 21:09:24 +00:00
parent aed9c4e6b9
commit 00ed162f6a
3 changed files with 416 additions and 212 deletions

View File

@ -1,6 +1,12 @@
# Паспорт прочитал Я выбрал экран таблицей # Годовой план закупок — экран сравнения позиций
Паспорт прочитал. Я выбрал экран с таблицей, а не конкретную платформу, Alem можно оставить как вашу рекомендацию. Сохранение решений и комментариев нужно сразу, не в будущих версиях. И добавь, что реальные данные обрабатываем только внутри КТ. Паспорт прочитал. Я выбрал экран с таблицей сравнения позиций годового плана закупок, а не конкретную платформу. Alem можно оставить как рекомендацию. Сохранение решений и комментариев нужно сразу, не в будущих версиях. Реальные данные обрабатываем только внутри КТ.
## Бизнес-процесс
Сравнение позиций годового плана закупок по группам (ИТ-оборудование, Сетевое оборудование, ПО, Услуги). Каждая позиция имеет характеристики, срок поставки, город, цену, поставщика и оценку совместимости.
Ответственный сравнивает позиции, проверяет совместимость, исключает несовместимые позиции и принимает решения с комментариями. Массовые действия: согласовать все совместимые, исключить несовместимые. Выгрузка отчёта в Excel.
## Объём работы ## Объём работы

View File

@ -3,12 +3,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <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-fonts.css">
<link rel="stylesheet" href="./design-system/kt-ai-tokens.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-components.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.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>"> <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> <style>
.kt-ai-app { min-height: 100vh; background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); } .kt-ai-app { min-height: 100vh; background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); }
.kt-ai-header-content { display: flex; align-items: center; gap: var(--kt-ai-space-4); } .kt-ai-header-content { display: flex; align-items: center; gap: var(--kt-ai-space-4); }
@ -29,10 +29,15 @@
.kt-ai-table tr:last-child td { border-bottom: none; } .kt-ai-table tr:last-child td { border-bottom: none; }
.kt-ai-table tr:hover td { background: var(--kt-ai-bg-soft); } .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 .num { text-align: right; font-variant-numeric: tabular-nums; }
.kt-ai-table .group-row { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); cursor: pointer; }
.kt-ai-table .group-row:hover { background: var(--kt-ai-border); }
.kt-ai-table .group-icon { display: inline-block; width: 20px; text-align: center; margin-right: var(--kt-ai-space-2); transition: transform 0.2s; }
.kt-ai-table .group-row.collapsed .group-icon { transform: rotate(-90deg); }
.kt-ai-table .group-row.collapsed + tr { display: none; }
.kt-ai-empty { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); } .kt-ai-empty { text-align: center; padding: var(--kt-ai-space-10); color: var(--kt-ai-fg-muted); }
.kt-ai-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; } .kt-ai-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.kt-ai-modal-overlay.in { display: flex; } .kt-ai-modal-overlay.in { display: flex; }
.kt-ai-modal { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); max-width: 560px; width: 90%; max-height: 90vh; overflow-y: auto; } .kt-ai-modal { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-6); max-width: 640px; width: 90%; max-height: 90vh; overflow-y: auto; }
.kt-ai-modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-5); } .kt-ai-modal-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--kt-ai-space-5); }
.kt-ai-modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); } .kt-ai-modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); }
.kt-ai-modal-close { background: none; border: none; font-size: var(--kt-ai-text-2xl); color: var(--kt-ai-fg-muted); cursor: pointer; padding: 0; line-height: 1; } .kt-ai-modal-close { background: none; border: none; font-size: var(--kt-ai-text-2xl); color: var(--kt-ai-fg-muted); cursor: pointer; padding: 0; line-height: 1; }
@ -42,13 +47,22 @@
.kt-ai-info-row:last-of-type { border-bottom: none; } .kt-ai-info-row:last-of-type { border-bottom: none; }
.kt-ai-info-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); } .kt-ai-info-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.kt-ai-info-value { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); } .kt-ai-info-value { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
.kt-ai-compare-block { background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); margin: var(--kt-ai-space-3) 0; }
.kt-ai-compare-title { font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); }
.kt-ai-compare-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--kt-ai-space-3); }
.kt-ai-compare-item { font-size: var(--kt-ai-text-xs); }
.kt-ai-compare-item .label { color: var(--kt-ai-fg-faint); margin-bottom: var(--kt-ai-space-1); }
.kt-ai-compare-item .value { font-weight: var(--kt-ai-weight-medium); }
.kt-ai-modal-actions { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); } .kt-ai-modal-actions { display: flex; gap: var(--kt-ai-space-3); margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-5); border-top: 1px solid var(--kt-ai-border); }
.kt-ai-decision-note { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); } .kt-ai-decision-note { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-2); }
.kt-ai-actions-bar { display: flex; justify-content: space-between; align-items: center; margin-top: var(--kt-ai-space-5); padding-top: var(--kt-ai-space-4); border-top: 1px solid var(--kt-ai-border); }
.kt-ai-bulk-actions { display: flex; gap: var(--kt-ai-space-3); }
@media (max-width: 768px) { @media (max-width: 768px) {
.kt-ai-kpi-strip { grid-template-columns: repeat(2, 1fr); } .kt-ai-kpi-strip { grid-template-columns: repeat(2, 1fr); }
.kt-ai-filters { flex-direction: column; align-items: stretch; } .kt-ai-filters { flex-direction: column; align-items: stretch; }
.kt-ai-filter-group { width: 100%; } .kt-ai-filter-group { width: 100%; }
.kt-ai-select { width: 100%; } .kt-ai-select { width: 100%; }
.kt-ai-compare-grid { grid-template-columns: 1fr; }
} }
</style> </style>
</head> </head>
@ -58,7 +72,7 @@
<div class="kt-ai-wrap kt-ai-pagehead-in"> <div class="kt-ai-wrap kt-ai-pagehead-in">
<div class="kt-ai-header-content"> <div class="kt-ai-header-content">
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28"> <img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
<strong>Система принятия решений</strong> <strong>Годовой план закупок</strong>
</div> </div>
<span class="kt-ai-spacer"></span> <span class="kt-ai-spacer"></span>
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span> <span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
@ -70,81 +84,86 @@
<section class="kt-ai-kpi-strip"> <section class="kt-ai-kpi-strip">
<div class="kt-ai-kpi"> <div class="kt-ai-kpi">
<div class="value" id="kpi-total">0</div> <div class="value" id="kpi-total">0</div>
<div class="label">Всего заявок</div> <div class="label">Всего позиций</div>
<div class="hint">за выбранный период</div> <div class="hint">в плане закупок</div>
</div> </div>
<div class="kt-ai-kpi"> <div class="kt-ai-kpi">
<div class="value" id="kpi-review">0</div> <div class="value" id="kpi-review">0</div>
<div class="label">На рассмотрении</div> <div class="label">На согласовании</div>
<div class="hint">требуют решения</div> <div class="hint">требуют решения</div>
</div> </div>
<div class="kt-ai-kpi"> <div class="kt-ai-kpi">
<div class="value" id="kpi-approved" style="color: var(--kt-ai-status-ok-fg)">0</div> <div class="value" id="kpi-approved" style="color: var(--kt-ai-status-ok-fg)">0</div>
<div class="label">Принято</div> <div class="label">Согласовано</div>
<div class="hint">за выбранный период</div> <div class="hint">включено в план</div>
</div> </div>
<div class="kt-ai-kpi"> <div class="kt-ai-kpi">
<div class="value" id="kpi-rejected" style="color: var(--kt-ai-status-risk-fg)">0</div> <div class="value" id="kpi-excluded" style="color: var(--kt-ai-status-risk-fg)">0</div>
<div class="label">Отклонено</div> <div class="label">Исключено</div>
<div class="hint">за выбранный период</div> <div class="hint">несовместимые позиции</div>
</div> </div>
</section> </section>
<section class="kt-ai-filters"> <section class="kt-ai-filters">
<div class="kt-ai-filter-group"> <div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Период</label> <label class="kt-ai-filter-label">Группа</label>
<select class="kt-ai-select" id="filter-period" style="min-width: 140px"> <select class="kt-ai-select" id="filter-group" style="min-width: 160px">
<option value="7d">7 дней</option> <option value="all">Все группы</option>
<option value="30d">30 дней</option> <option value="it">ИТ-оборудование</option>
<option value="quarter">Квартал</option> <option value="network">Сетевое оборудование</option>
<option value="year">Год</option> <option value="software">Программное обеспечение</option>
<option value="services">Услуги</option>
</select> </select>
</div> </div>
<div class="kt-ai-filter-group"> <div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Статус</label> <label class="kt-ai-filter-label">Статус</label>
<select class="kt-ai-select" id="filter-status" style="min-width: 160px"> <select class="kt-ai-select" id="filter-status" style="min-width: 160px">
<option value="all">Все статусы</option> <option value="all">Все статусы</option>
<option value="new">Новый</option> <option value="review">На согласовании</option>
<option value="review">На рассмотрении</option> <option value="approved">Согласовано</option>
<option value="decided">Решение принято</option>
<option value="rejected">Отклонено</option> <option value="rejected">Отклонено</option>
<option value="excluded">Исключено</option>
</select> </select>
</div> </div>
<div class="kt-ai-filter-group"> <div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Тип заявки</label> <label class="kt-ai-filter-label">Город</label>
<select class="kt-ai-select" id="filter-type" style="min-width: 160px"> <select class="kt-ai-select" id="filter-city" style="min-width: 140px">
<option value="all">Все типы</option> <option value="all">Все города</option>
<option value="access">Доступ к данным</option> <option value="almaty">Алматы</option>
<option value="integration">Интеграция</option> <option value="astana">Астана</option>
<option value="hardware">Оборудование</option> <option value="shymkent">Шымкент</option>
<option value="other">Другое</option> <option value="other">Другие</option>
</select> </select>
</div> </div>
<div class="kt-ai-filter-group"> <div class="kt-ai-filter-group">
<label class="kt-ai-filter-label">Приоритет</label> <label class="kt-ai-filter-label">Совместимость</label>
<select class="kt-ai-select" id="filter-priority" style="min-width: 140px"> <select class="kt-ai-select" id="filter-compat" style="min-width: 160px">
<option value="all">Все приоритеты</option> <option value="all">Все</option>
<option value="high">Высокий</option> <option value="compatible">Совместима</option>
<option value="medium">Средний</option> <option value="check">Требует проверки</option>
<option value="low">Низкий</option> <option value="incompatible">Несовместима</option>
</select> </select>
</div> </div>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="btn-export" onclick="exportToExcel()">📄 Выгрузить Excel</button>
</section> </section>
<section class="kt-ai-table-wrap"> <section class="kt-ai-table-wrap">
<table class="kt-ai-table"> <table class="kt-ai-table">
<thead> <thead>
<tr> <tr>
<th>Дата</th> <th style="width: 40px"></th>
<th>Заявитель</th> <th>Наименование</th>
<th>Тип</th> <th>Характеристики</th>
<th>Приоритет</th> <th>Срок</th>
<th>Город</th>
<th class="num">Цена (₸)</th>
<th>Совместимость</th>
<th>Статус</th> <th>Статус</th>
<th>Решение</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody id="requests-table-body"> <tbody id="positions-table-body">
</tbody> </tbody>
</table> </table>
<div id="empty-state" class="kt-ai-empty" style="display: none"> <div id="empty-state" class="kt-ai-empty" style="display: none">
@ -153,24 +172,39 @@
</div> </div>
</section> </section>
<div class="kt-ai-actions-bar">
<div class="kt-ai-bulk-actions">
<button class="kt-ai-btn" data-variant="primary" id="btn-approve-all" onclick="approveAll()">✓ Согласовать все совместимые</button>
<button class="kt-ai-btn" data-variant="danger" id="btn-exclude-incompatible" onclick="excludeIncompatible()">✗ Исключить несовместимые</button>
</div>
<p class="kt-ai-decision-note" id="bulk-result"></p>
</div>
</main> </main>
<div class="kt-ai-modal-overlay" id="modal-overlay"> <div class="kt-ai-modal-overlay" id="modal-overlay">
<div class="kt-ai-modal"> <div class="kt-ai-modal">
<div class="kt-ai-modal-head"> <div class="kt-ai-modal-head">
<h2 class="kt-ai-modal-title">Рассмотрение заявки</h2> <h2 class="kt-ai-modal-title">Согласование позиции</h2>
<button class="kt-ai-modal-close" id="modal-close">&times;</button> <button class="kt-ai-modal-close" id="modal-close">&times;</button>
</div> </div>
<div class="kt-ai-modal-body"> <div class="kt-ai-modal-body">
<div id="request-details"></div> <div id="position-details"></div>
<div class="kt-ai-compare-block">
<div class="kt-ai-compare-title">📊 Сравнение характеристик</div>
<div class="kt-ai-compare-grid" id="compare-grid"></div>
</div>
<div class="kt-ai-field"> <div class="kt-ai-field">
<label for="decision-comment">Комментарий к решению</label> <label for="decision-comment">Комментарий к решению</label>
<textarea id="decision-comment" class="kt-ai-textarea" rows="3" placeholder="Обоснование решения..."></textarea> <textarea id="decision-comment" class="kt-ai-textarea" rows="3" placeholder="Обоснование решения..."></textarea>
</div> </div>
<div class="kt-ai-modal-actions"> <div class="kt-ai-modal-actions">
<button class="kt-ai-btn" data-variant="primary" id="btn-approve">Принять</button> <button class="kt-ai-btn" data-variant="primary" id="btn-approve">Согласовать</button>
<button class="kt-ai-btn" data-variant="danger" id="btn-reject">Отклонить</button> <button class="kt-ai-btn" data-variant="danger" id="btn-reject">Отклонить</button>
<button class="kt-ai-btn" data-variant="ghost" id="btn-rework">На доработку</button> <button class="kt-ai-btn" data-variant="ghost" id="btn-exclude">Исключить</button>
</div> </div>
<p class="kt-ai-decision-note">Решение будет сохранено в localStorage</p> <p class="kt-ai-decision-note">Решение будет сохранено в localStorage</p>
</div> </div>
@ -180,53 +214,50 @@
<script> <script>
// Реестр фильтров // Реестр фильтров
const FILTERS = [ const FILTERS = [
{ id: 'period', label: 'Период', affects: ['table', 'kpi', 'empty'] }, { id: 'group', label: 'Группа', affects: ['table', 'kpi', 'empty'] },
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'empty'] }, { id: 'status', label: 'Статус', affects: ['table', 'kpi', 'empty'] },
{ id: 'type', label: 'Тип', affects: ['table', 'kpi', 'empty'] }, { id: 'city', label: 'Город', affects: ['table', 'kpi', 'empty'] },
{ id: 'priority', label: 'Приоритет', affects: ['table', 'kpi', 'empty'] } { id: 'compat', label: 'Совместимость', affects: ['table', 'kpi', 'empty'] }
]; ];
// Демо-данные // Демо-данные по группам
const DEMO_DATA = { const DEMO_DATA = {
'7d': [ 'it': [
{ id: 1, date: '2026-09-10', applicant: 'Иванов А.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null }, { id: 1, group: 'it', groupName: 'ИТ-оборудование', name: 'Сервер Dell PowerEdge R750', specs: '2xCPU 16-core, 128GB RAM, 4TB SSD', deadline: '2026-12-15', city: 'almaty', cityName: 'Алматы', price: 4500000, qty: 2, supplier: 'Dell Technologies', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 2, date: '2026-09-11', applicant: 'Петрова М.К.', department: 'Отдел аналитики', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null }, { id: 2, group: 'it', groupName: 'ИТ-оборудование', name: 'Рабочая станция HP Z4 G5', specs: 'CPU i9-13900K, 64GB RAM, 2TB NVMe', deadline: '2026-11-30', city: 'astana', cityName: 'Астана', price: 1200000, qty: 5, supplier: 'HP Inc.', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 3, date: '2026-09-12', applicant: 'Сидоров Е.В.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Согласовано в рамках бюджета' }, { id: 3, group: 'it', groupName: 'ИТ-оборудование', name: 'Ноутбук Lenovo ThinkPad P16', specs: 'CPU i7-13850HX, 32GB RAM, 1TB SSD', deadline: '2026-12-01', city: 'shymkent', cityName: 'Шымкент', price: 850000, qty: 10, supplier: 'Lenovo', compat: 'check', compatName: 'Требует проверки', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
{ id: 4, date: '2026-09-13', applicant: 'Алиев Н.Р.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 5, date: '2026-09-14', applicant: 'Козлова Д.А.', department: 'Бухгалтерия', type: 'other', typeName: 'Другое', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null }
], ],
'30d': [ 'network': [
{ id: 6, date: '2026-09-01', applicant: 'Нурсултанов Б.Т.', department: 'Отдел маркетинга', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция одобрена' }, { id: 4, group: 'network', groupName: 'Сетевое оборудование', name: 'Маршрутизатор Cisco ISR 4351', specs: '3xGE WAN, 8xGE LAN, IOS XE', deadline: '2027-01-15', city: 'almaty', cityName: 'Алматы', price: 2800000, qty: 3, supplier: 'Cisco', compat: 'compatible', compatName: 'Совместима', status: 'approved', statusName: 'Согласовано', decision: 'approved', comment: 'Совместимость подтверждена' },
{ id: 7, date: '2026-09-03', applicant: 'Омарова Г.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ предоставлен' }, { id: 5, group: 'network', groupName: 'Сетевое оборудование', name: 'Коммутатор Huawei S5735-L48', specs: '48xGE, 4x10G SFP+, PoE+', deadline: '2026-12-20', city: 'astana', cityName: 'Астана', price: 650000, qty: 8, supplier: 'Huawei', compat: 'incompatible', compatName: 'Несовместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 8, date: '2026-09-05', applicant: 'Васильев П.И.', department: 'Служба поддержки', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null }, { id: 6, group: 'network', groupName: 'Сетевое оборудование', name: 'СКС-комплект 19"', specs: 'Шкаф 42U, патч-панели, кабель', deadline: '2026-11-25', city: 'other', cityName: 'Караганда', price: 380000, qty: 4, supplier: 'Legrand', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
{ id: 9, date: '2026-09-07', applicant: 'Жанарбеков А.К.', department: 'Отдел продаж', type: 'other', typeName: 'Другое', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null },
{ id: 10, date: '2026-09-09', applicant: 'Смирнова О.Л.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 11, date: '2026-09-10', applicant: 'Иванов А.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 12, date: '2026-09-11', applicant: 'Петрова М.К.', department: 'Отдел аналитики', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'review', statusName: 'На рассмотрении', decision: null, comment: null },
{ id: 13, date: '2026-09-12', applicant: 'Сидоров Е.В.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Согласовано в рамках бюджета' }
], ],
'quarter': [ 'software': [
{ id: 14, date: '2026-07-15', applicant: 'Ахметов Р.Б.', department: 'Департамент ИТ', type: 'integration', typeName: 'Интеграция', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция завершена' }, { id: 7, group: 'software', groupName: 'Программное обеспечение', name: 'Лицензия Windows Server 2022', specs: 'Datacenter, 16 core, CAL', deadline: '2026-12-10', city: 'almaty', cityName: 'Алматы', price: 450000, qty: 5, supplier: 'Microsoft', compat: 'compatible', compatName: 'Совместима', status: 'approved', statusName: 'Согласовано', decision: 'approved', comment: 'Стандартная лицензия' },
{ id: 15, date: '2026-08-01', applicant: 'Кузнецова А.В.', department: 'Отдел кадров', type: 'access', typeName: 'Доступ к данным', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ настроен' }, { id: 8, group: 'software', groupName: 'Программное обеспечение', name: 'Антивирус Kaspersky Endpoint', specs: '3 года, 500 устройств', deadline: '2027-01-05', city: 'astana', cityName: 'Астана', price: 1200000, qty: 1, supplier: 'Kaspersky', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 16, date: '2026-08-10', applicant: 'Токтаев К.С.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Оборудование закуплено' }, { id: 9, group: 'software', groupName: 'Программное обеспечение', name: '1С:Предприятие 8.3', specs: 'Сервер + 50 клиентских лицензий', deadline: '2026-12-05', city: 'shymkent', cityName: 'Шымкент', price: 980000, qty: 1, supplier: '1С', compat: 'check', compatName: 'Требует проверки', status: 'review', statusName: 'На согласовании', decision: null, comment: null }
{ id: 17, date: '2026-08-20', applicant: 'Лебедева М.А.', department: 'Бухгалтерия', type: 'other', typeName: 'Другое', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null },
{ id: 18, date: '2026-09-01', applicant: 'Нурсултанов Б.Т.', department: 'Отдел маркетинга', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция одобрена' },
{ id: 19, date: '2026-09-05', applicant: 'Васильев П.И.', department: 'Служба поддержки', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'new', statusName: 'Новый', decision: null, comment: null }
], ],
'year': [] // Пустой срез для демонстрации пустого состояния 'services': [
{ id: 10, group: 'services', groupName: 'Услуги', name: 'Техническая поддержка 24/7', specs: '3 года, SLA 99.9%', deadline: '2027-02-01', city: 'almaty', cityName: 'Алматы', price: 5400000, qty: 1, supplier: 'КТ-Сервис', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 11, group: 'services', groupName: 'Услуги', name: 'Обучение персонала', specs: 'Курсы Cisco CCNA, 20 человек', deadline: '2027-03-15', city: 'astana', cityName: 'Астана', price: 2000000, qty: 1, supplier: 'УЦ "Сетевые Технологии"', compat: 'compatible', compatName: 'Совместима', status: 'review', statusName: 'На согласовании', decision: null, comment: null },
{ id: 12, group: 'services', groupName: 'Услуги', name: 'Аутсорсинг ИТ-инфраструктуры', specs: '2 года, полный спектр услуг', deadline: '2027-01-20', city: 'other', cityName: 'Актобе', price: 8600000, qty: 1, supplier: 'КТ-Аутсорсинг', compat: 'incompatible', compatName: 'Несовместима', status: 'excluded', statusName: 'Исключено', decision: 'excluded', comment: 'Дублирует внутренние ресурсы' }
]
}; };
// Пустой срез для демонстрации
DEMO_DATA['other'] = [];
// Загрузка сохранённых решений из localStorage // Загрузка сохранённых решений из localStorage
function loadSavedDecisions() { function loadSavedDecisions() {
const saved = localStorage.getItem('requestDecisions'); const saved = localStorage.getItem('procurementDecisions');
return saved ? JSON.parse(saved) : {}; return saved ? JSON.parse(saved) : {};
} }
// Сохранение решения в localStorage // Сохранение решения в localStorage
function saveDecision(requestId, decision, comment) { function saveDecision(positionId, decision, comment) {
const decisions = loadSavedDecisions(); const decisions = loadSavedDecisions();
decisions[requestId] = { decision, comment, date: new Date().toISOString() }; decisions[positionId] = { decision, comment, date: new Date().toISOString() };
localStorage.setItem('requestDecisions', JSON.stringify(decisions)); localStorage.setItem('procurementDecisions', JSON.stringify(decisions));
} }
// Применение сохранённых решений к демо-данным // Применение сохранённых решений к демо-данным
@ -235,12 +266,21 @@ function applySavedDecisions(data) {
return data.map(item => { return data.map(item => {
if (decisions[item.id]) { if (decisions[item.id]) {
const saved = decisions[item.id]; const saved = decisions[item.id];
const statusMap = {
'approved': 'approved',
'rejected': 'rejected',
'excluded': 'excluded'
};
const statusNameMap = {
'approved': 'Согласовано',
'rejected': 'Отклонено',
'excluded': 'Исключено'
};
return { return {
...item, ...item,
decision: saved.decision, decision: saved.decision,
decisionName: saved.decision === 'approved' ? 'Принято' : saved.decision === 'rejected' ? 'Отклонено' : 'На доработку', status: statusMap[saved.decision] || item.status,
status: 'decided', statusName: statusNameMap[saved.decision] || item.statusName,
statusName: 'Решение принято',
comment: saved.comment comment: saved.comment
}; };
} }
@ -248,34 +288,41 @@ function applySavedDecisions(data) {
}); });
} }
// Получение данных по фильтру периода // Получение всех данных
function getDataForPeriod(period) { function getAllData() {
const baseData = DEMO_DATA[period] || []; let allData = [];
return applySavedDecisions(baseData); for (const group in DEMO_DATA) {
allData = allData.concat(DEMO_DATA[group]);
}
return applySavedDecisions(allData);
} }
// Фильтрация данных // Фильтрация данных
function filterData(data, filters) { function filterData(data, filters) {
let filtered = [...data]; let filtered = [...data];
if (filters.group !== 'all') {
filtered = filtered.filter(item => item.group === filters.group);
}
if (filters.status !== 'all') { if (filters.status !== 'all') {
if (filters.status === 'new') { if (filters.status === 'review') {
filtered = filtered.filter(item => item.status === 'new');
} else if (filters.status === 'review') {
filtered = filtered.filter(item => item.status === 'review'); filtered = filtered.filter(item => item.status === 'review');
} else if (filters.status === 'decided') { } else if (filters.status === 'approved') {
filtered = filtered.filter(item => item.status === 'decided' && item.decision === 'approved'); filtered = filtered.filter(item => item.status === 'approved');
} else if (filters.status === 'rejected') { } else if (filters.status === 'rejected') {
filtered = filtered.filter(item => item.status === 'decided' && item.decision === 'rejected'); filtered = filtered.filter(item => item.status === 'rejected');
} else if (filters.status === 'excluded') {
filtered = filtered.filter(item => item.status === 'excluded');
} }
} }
if (filters.type !== 'all') { if (filters.city !== 'all') {
filtered = filtered.filter(item => item.type === filters.type); filtered = filtered.filter(item => item.city === filters.city);
} }
if (filters.priority !== 'all') { if (filters.compat !== 'all') {
filtered = filtered.filter(item => item.priority === filters.priority); filtered = filtered.filter(item => item.compat === filters.compat);
} }
return filtered; return filtered;
@ -285,9 +332,9 @@ function filterData(data, filters) {
function calculateKPI(data) { function calculateKPI(data) {
const total = data.length; const total = data.length;
const review = data.filter(item => item.status === 'review').length; const review = data.filter(item => item.status === 'review').length;
const approved = data.filter(item => item.decision === 'approved').length; const approved = data.filter(item => item.status === 'approved').length;
const rejected = data.filter(item => item.decision === 'rejected').length; const excluded = data.filter(item => item.status === 'excluded').length;
return { total, review, approved, rejected }; return { total, review, approved, excluded };
} }
// Отрисовка KPI // Отрисовка KPI
@ -295,36 +342,50 @@ function renderKPI(kpi) {
document.getElementById('kpi-total').textContent = kpi.total; document.getElementById('kpi-total').textContent = kpi.total;
document.getElementById('kpi-review').textContent = kpi.review; document.getElementById('kpi-review').textContent = kpi.review;
document.getElementById('kpi-approved').textContent = kpi.approved; document.getElementById('kpi-approved').textContent = kpi.approved;
document.getElementById('kpi-rejected').textContent = kpi.rejected; document.getElementById('kpi-excluded').textContent = kpi.excluded;
} }
// Цвет статуса // Цвет статуса
function getStatusChip(status, decision) { function getStatusChip(status) {
const chips = { const chips = {
'new': '<span class="kt-ai-chip" data-tone="blue">Новый</span>', 'review': '<span class="kt-ai-chip" data-tone="orange">На согласовании</span>',
'review': '<span class="kt-ai-chip" data-tone="orange">На рассмотрении</span>', 'approved': '<span class="kt-ai-chip" data-tone="green">Согласовано</span>',
'decided': decision === 'approved' 'rejected': '<span class="kt-ai-chip" data-tone="red">Отклонено</span>',
? '<span class="kt-ai-chip" data-tone="green">Принято</span>' 'excluded': '<span class="kt-ai-chip" data-tone="red">Исключено</span>'
: decision === 'rejected'
? '<span class="kt-ai-chip" data-tone="red">Отклонено</span>'
: '<span class="kt-ai-chip" data-tone="orange">На доработку</span>'
}; };
return chips[status] || chips['new']; return chips[status] || chips['review'];
} }
// Цвет приоритета // Цвет совместимости
function getPriorityChip(priority) { function getCompatChip(compat) {
const chips = { const chips = {
'high': '<span class="kt-ai-chip" data-tone="red">Высокий</span>', 'compatible': '<span class="kt-ai-chip" data-tone="green">Совместима</span>',
'medium': '<span class="kt-ai-chip" data-tone="orange">Средний</span>', 'check': '<span class="kt-ai-chip" data-tone="orange">Требует проверки</span>',
'low': '<span class="kt-ai-chip" data-tone="blue">Низкий</span>' 'incompatible': '<span class="kt-ai-chip" data-tone="red">Несовместима</span>'
}; };
return chips[priority] || chips['medium']; return chips[compat] || chips['check'];
} }
// Отрисовка таблицы // Форматирование цены
function formatPrice(price) {
return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
// Группировка данных по категориям
function groupByCategory(data) {
const groups = {};
data.forEach(item => {
if (!groups[item.group]) {
groups[item.group] = [];
}
groups[item.group].push(item);
});
return groups;
}
// Отрисовка таблицы с группировкой
function renderTable(data) { function renderTable(data) {
const tbody = document.getElementById('requests-table-body'); const tbody = document.getElementById('positions-table-body');
const emptyState = document.getElementById('empty-state'); const emptyState = document.getElementById('empty-state');
if (data.length === 0) { if (data.length === 0) {
@ -334,37 +395,71 @@ function renderTable(data) {
} }
emptyState.style.display = 'none'; emptyState.style.display = 'none';
tbody.innerHTML = data.map(item => ` const grouped = groupByCategory(data);
<tr>
<td>${item.date}</td> let html = '';
<td> for (const groupKey in grouped) {
<div style="font-weight: var(--kt-ai-weight-medium)">${item.applicant}</div> const items = grouped[groupKey];
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.department}</div> if (items.length === 0) continue;
</td>
<td>${item.typeName}</td> const groupName = items[0].groupName;
<td>${getPriorityChip(item.priority)}</td> html += `
<td>${getStatusChip(item.status, item.decision)}</td> <tr class="group-row" onclick="toggleGroup('${groupKey}')">
<td>${item.decision ? (item.decision === 'approved' ? '<span style="color: var(--kt-ai-status-ok-fg)">✓</span>' : '<span style="color: var(--kt-ai-status-risk-fg)">✗</span>') : '—'}</td> <td><span class="group-icon">▼</span></td>
<td class="num"> <td colspan="8">${groupName} (${items.length} поз.)</td>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openModal(${item.id})">Рассмотреть</button> </tr>
</td> `;
</tr>
`).join(''); items.forEach(item => {
html += `
<tr class="group-item ${groupKey}">
<td></td>
<td>
<div style="font-weight: var(--kt-ai-weight-medium)">${item.name}</div>
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.supplier}</div>
</td>
<td style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.specs}</td>
<td>${item.deadline}</td>
<td>${item.cityName}</td>
<td class="num">${formatPrice(item.price)} ₸</td>
<td>${getCompatChip(item.compat)}</td>
<td>${getStatusChip(item.status)}</td>
<td class="num">
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openModal(${item.id})">Согласовать</button>
</td>
</tr>
`;
});
}
tbody.innerHTML = html;
}
// Свёртка/развёртка группы
function toggleGroup(groupKey) {
const rows = document.querySelectorAll(`.group-item.${groupKey}`);
const groupRow = document.querySelector(`tr[onclick="toggleGroup('${groupKey}')"]`);
rows.forEach(row => {
row.style.display = row.style.display === 'none' ? '' : 'none';
});
groupRow.classList.toggle('collapsed');
} }
// Текущие фильтры // Текущие фильтры
let currentFilters = { let currentFilters = {
period: '7d', group: 'all',
status: 'all', status: 'all',
type: 'all', city: 'all',
priority: 'all' compat: 'all'
}; };
let currentRequestId = null; let currentPositionId = null;
// Обновление отображения // Обновление отображения
function updateDisplay() { function updateDisplay() {
const data = getDataForPeriod(currentFilters.period); const data = getAllData();
const filtered = filterData(data, currentFilters); const filtered = filterData(data, currentFilters);
const kpi = calculateKPI(filtered); const kpi = calculateKPI(filtered);
@ -373,69 +468,155 @@ function updateDisplay() {
} }
// Открытие модального окна // Открытие модального окна
function openModal(requestId) { function openModal(positionId) {
const data = getDataForPeriod(currentFilters.period); const data = getAllData();
const request = data.find(item => item.id === requestId); const position = data.find(item => item.id === positionId);
if (!request) return; if (!position) return;
currentRequestId = requestId; currentPositionId = positionId;
const details = document.getElementById('request-details'); const details = document.getElementById('position-details');
details.innerHTML = ` details.innerHTML = `
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Дата</span> <span class="kt-ai-info-label">Наименование</span>
<span class="kt-ai-info-value">${request.date}</span> <span class="kt-ai-info-value">${position.name}</span>
</div> </div>
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Заявитель</span> <span class="kt-ai-info-label">Группа</span>
<span class="kt-ai-info-value">${request.applicant}</span> <span class="kt-ai-info-value">${position.groupName}</span>
</div> </div>
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Отдел</span> <span class="kt-ai-info-label">Характеристики</span>
<span class="kt-ai-info-value">${request.department}</span> <span class="kt-ai-info-value">${position.specs}</span>
</div> </div>
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Тип заявки</span> <span class="kt-ai-info-label">Срок поставки</span>
<span class="kt-ai-info-value">${request.typeName}</span> <span class="kt-ai-info-value">${position.deadline}</span>
</div> </div>
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Приоритет</span> <span class="kt-ai-info-label">Город</span>
<span class="kt-ai-info-value">${getPriorityChip(request.priority)}</span> <span class="kt-ai-info-value">${position.cityName}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Цена</span>
<span class="kt-ai-info-value">${formatPrice(position.price)} ₸</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Количество</span>
<span class="kt-ai-info-value">${position.qty} шт.</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Поставщик</span>
<span class="kt-ai-info-value">${position.supplier}</span>
</div>
<div class="kt-ai-info-row">
<span class="kt-ai-info-label">Совместимость</span>
<span class="kt-ai-info-value">${getCompatChip(position.compat)}</span>
</div> </div>
<div class="kt-ai-info-row"> <div class="kt-ai-info-row">
<span class="kt-ai-info-label">Статус</span> <span class="kt-ai-info-label">Статус</span>
<span class="kt-ai-info-value">${getStatusChip(request.status, request.decision)}</span> <span class="kt-ai-info-value">${getStatusChip(position.status)}</span>
</div> </div>
`; `;
document.getElementById('decision-comment').value = request.comment || ''; // Сравнение характеристик
const compareGrid = document.getElementById('compare-grid');
const specs = position.specs.split(',').map(s => s.trim());
compareGrid.innerHTML = specs.map(spec => `
<div class="kt-ai-compare-item">
<div class="label">Параметр</div>
<div class="value">${spec}</div>
</div>
`).join('');
document.getElementById('decision-comment').value = position.comment || '';
document.getElementById('modal-overlay').classList.add('in'); document.getElementById('modal-overlay').classList.add('in');
} }
// Закрытие модального окна // Закрытие модального окна
function closeModal() { function closeModal() {
document.getElementById('modal-overlay').classList.remove('in'); document.getElementById('modal-overlay').classList.remove('in');
currentRequestId = null; currentPositionId = null;
} }
// Сохранение решения // Сохранение решения
function makeDecision(decision) { function makeDecision(decision) {
if (!currentRequestId) return; if (!currentPositionId) return;
const comment = document.getElementById('decision-comment').value.trim(); const comment = document.getElementById('decision-comment').value.trim();
saveDecision(currentRequestId, decision, comment); saveDecision(currentPositionId, decision, comment);
closeModal(); closeModal();
updateDisplay(); updateDisplay();
}
// Массовое согласование совместимых
function approveAll() {
const data = getAllData();
const compatible = data.filter(item => item.compat === 'compatible' && item.status === 'review');
// Показать уведомление let count = 0;
alert('Решение сохранено!'); compatible.forEach(item => {
saveDecision(item.id, 'approved', 'Автоматически согласовано как совместимое');
count++;
});
document.getElementById('bulk-result').textContent = `Согласовано ${count} позиций`;
updateDisplay();
}
// Исключение несовместимых
function excludeIncompatible() {
const data = getAllData();
const incompatible = data.filter(item => item.compat === 'incompatible' && item.status !== 'excluded');
let count = 0;
incompatible.forEach(item => {
saveDecision(item.id, 'excluded', 'Исключено как несовместимое');
count++;
});
document.getElementById('bulk-result').textContent = `Исключено ${count} несовместимых позиций`;
updateDisplay();
}
// Выгрузка в Excel (имитация)
function exportToExcel() {
const data = getAllData();
const filtered = filterData(data, currentFilters);
// Создаём простой CSV (Excel откроет)
const headers = ['ID', 'Группа', 'Наименование', 'Характеристики', 'Срок', 'Город', 'Цена', 'Кол-во', 'Поставщик', 'Совместимость', 'Статус', 'Решение', 'Комментарий'];
const rows = filtered.map(item => [
item.id,
item.groupName,
item.name,
item.specs,
item.deadline,
item.cityName,
item.price,
item.qty,
item.supplier,
item.compatName,
item.statusName,
item.decision || '',
item.comment || ''
]);
const csv = [headers, ...rows].map(row => row.join(';')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'plan-zakupok.csv';
link.click();
URL.revokeObjectURL(url);
} }
// Обработчики фильтров // Обработчики фильтров
document.getElementById('filter-period').addEventListener('change', (e) => { document.getElementById('filter-group').addEventListener('change', (e) => {
currentFilters.period = e.target.value; currentFilters.group = e.target.value;
updateDisplay(); updateDisplay();
}); });
@ -444,13 +625,13 @@ document.getElementById('filter-status').addEventListener('change', (e) => {
updateDisplay(); updateDisplay();
}); });
document.getElementById('filter-type').addEventListener('change', (e) => { document.getElementById('filter-city').addEventListener('change', (e) => {
currentFilters.type = e.target.value; currentFilters.city = e.target.value;
updateDisplay(); updateDisplay();
}); });
document.getElementById('filter-priority').addEventListener('change', (e) => { document.getElementById('filter-compat').addEventListener('change', (e) => {
currentFilters.priority = e.target.value; currentFilters.compat = e.target.value;
updateDisplay(); updateDisplay();
}); });
@ -462,13 +643,17 @@ document.getElementById('modal-overlay').addEventListener('click', (e) => {
document.getElementById('btn-approve').addEventListener('click', () => makeDecision('approved')); document.getElementById('btn-approve').addEventListener('click', () => makeDecision('approved'));
document.getElementById('btn-reject').addEventListener('click', () => makeDecision('rejected')); document.getElementById('btn-reject').addEventListener('click', () => makeDecision('rejected'));
document.getElementById('btn-rework').addEventListener('click', () => makeDecision('rework')); document.getElementById('btn-exclude').addEventListener('click', () => makeDecision('excluded'));
// Инициализация // Инициализация
updateDisplay(); updateDisplay();
// Сделать функцию доступной глобально // Сделать функции доступными глобально
window.openModal = openModal; window.openModal = openModal;
window.toggleGroup = toggleGroup;
window.approveAll = approveAll;
window.excludeIncompatible = excludeIncompatible;
window.exportToExcel = exportToExcel;
</script> </script>
</body> </body>

View File

@ -1,8 +1,8 @@
# Паспорт бизнес-процесса: Система принятия решений по заявкам # Паспорт бизнес-процесса: Согласование годового плана закупок
## Назначение системы ## Назначение системы
Система для обработки и принятия решений по входящим заявкам сотрудников. Позволяет ответственным лицам рассматривать заявки, принимать решения (принято/отклонено), добавлять комментарии и отслеживать статусы. Система для согласования позиций годового плана закупок. Позволяет ответственным лицам сравнивать характеристики позиций, проверять совместимость, исключать неподходящие позиции и принимать решения с комментариями.
## Важные принципы ## Важные принципы
@ -12,70 +12,82 @@
## Сущности ## Сущности
### Заявка ### Позиция плана закупок
- **ID** — уникальный номер - **ID** — уникальный номер позиции
- **Дата создания** — когда поступила заявка - **Группа/Категория** — группа закупок (ИТ-оборудование, Сетевое оборудование, ПО, Услуги)
- **Заявитель** — ФИО сотрудника - **Наименование** — название позиции
- **Отдел/Департамент** — подразделение заявителя - **Характеристики** — технические параметры (мощность, ёмкость, совместимость)
- **Тип заявки** — доступ к данным / интеграция / оборудование / другое - **Срок поставки** — планируемая дата поставки
- **Описание** — краткое описание потребности - **Город** — место доставки
- **Приоритет** — низкий / средний / высокий - **Цена** — стоимость в тенге
- **Статус** — новый / на рассмотрении / решение принято - **Количество** — единиц
- **Поставщик** — рекомендуемый поставщик
- **Совместимость** — совместима / требует проверки / несовместима
- **Статус согласования** — на согласовании / согласовано / отклонено / исключено
### Решение ### Решение по позиции
- **ID заявки** — ссылка на заявку - **ID позиции** — ссылка на позицию
- **Решение** — принято / отклонено / на доработку - **Решение** — согласовано / отклонено / исключено / на доработку
- **Комментарий** — обоснование решения - **Комментарий** — обоснование решения
- **Кто принял** — ФИО ответственного - **Кто принял** — ФИО ответственного
- **Дата решения** — когда принято решение - **Дата решения** — когда принято решение
## Статусы заявки ## Статусы позиции
| Статус | Описание | Что можно делать | | Статус | Описание | Что можно делать |
|--------|----------|------------------| |--------|----------|------------------|
| Новый | Заявка только поступила | Назначить ответственного, взять в работу | | На согласовании | Позиция ожидает решения | Сравнить, проверить совместимость, принять решение |
| На рассмотрении | Ответственный изучает заявку | Запросить доп. информацию, принять решение | | Согласовано | Позиция одобрена | Просмотр, включение в итоговый план |
| Решение принято | Решение зафиксировано | Просмотр, экспорт в отчёт | | Отклонено | Позиция отклонена | Просмотр, обоснование отклонения |
| Отклонено | Заявка отклонена | Просмотр, архивирование | | Исключено | Позиция исключена из плана | Просмотр, причина исключения |
## Роли пользователей ## Группы позиций
| Роль | Права | | Группа | Примеры |
|------|-------| |--------|---------|
| Заявитель | Создать заявку, просмотр своих заявок | | ИТ-оборудование | Серверы, рабочие станции, ноутбуки |
| Ответственный | Просмотр назначенных заявок, принятие решений, комментарии | | Сетевое оборудование | Маршрутизаторы, коммутаторы, СКС |
| Администратор | Все заявки, назначение ответственных, экспорт данных | | Программное обеспечение | Лицензии ПО, антивирусы, ОС |
| Услуги | Техническая поддержка, обучение, аутсорсинг |
## Интерфейс — экран с таблицей ## Интерфейс — экран сравнения позиций
### KPI-полоса (верхняя панель) ### KPI-полоса (верхняя панель)
- Всего заявок - Всего позиций
- На рассмотрении - На согласовании
- Принято за период - Согласовано
- Отклонено за период - Исключено (несовместимые)
### Фильтры ### Фильтры
- Период (7 дней / 30 дней / квартал / год) - Группа (все / ИТ-оборудование / Сетевое / ПО / Услуги)
- Статус (все / новый / на рассмотрении / решение принято / отклонено) - Статус (все / на согласовании / согласовано / отклонено / исключено)
- Тип заявки (все / доступ / интеграция / оборудование / другое) - Город (все / Алматы / Астана / Шымкент / другие)
- Приоритет (все / высокий / средний / низкий) - Совместимость (все / совместима / требует проверки / несовместима)
### Таблица заявок ### Таблица позиций (группированная)
Колонки: Колонки:
1. Дата 1. Группа (свёртка/развёртка)
2. Заявитель (с отделом) 2. Наименование
3. Тип заявки 3. Характеристики
4. Приоритет (цветной чип) 4. Срок поставки
5. Статус (цветной чип) 5. Город
6. Решение (если принято) 6. Цена (тенге)
7. Действия (кнопка «Рассмотреть») 7. Совместимость (цветной чип)
8. Статус (цветной чип)
9. Действия (кнопка «Согласовать», «Исключить»)
### Карточка рассмотрения (открывается по клику) ### Карточка согласования (открывается по клику)
- Полная информация о заявке - Полная информация о позиции
- Сравнение с аналогами (если есть)
- Проверка совместимости
- Поле для комментария - Поле для комментария
- Кнопки: «Принять» / «Отклонить» / «На доработку» - Кнопки: «Согласовать» / «Отклонить» / «Исключить»
- Сохранение в localStorage сразу после нажатия - Сохранение в localStorage сразу после нажатия
### Выгрузка в Excel
- Кнопка «Выгрузить отчёт» — генерация Excel-файла с текущими данными
- Формат: XLSX, все поля позиции + решения + комментарии
## Хранение данных ## Хранение данных
### Демо-данные ### Демо-данные
@ -97,6 +109,7 @@
5. **Пустое состояние** — если нет данных под фильтр, показать причину 5. **Пустое состояние** — если нет данных под фильтр, показать причину
6. **Фильтры меняют всё** — таблица, KPI, текст объяснения 6. **Фильтры меняют всё** — таблица, KPI, текст объяснения
7. **Один пустой срез** — хотя бы один фильтр показывает «нет данных» 7. **Один пустой срез** — хотя бы один фильтр показывает «нет данных»
8. **Группировка строк** — свёртка/развёртка по категориям
## Запреты ## Запреты