Экран сравнения позиций годового плана закупок
This commit is contained in:
parent
aed9c4e6b9
commit
00ed162f6a
10
brief.md
10
brief.md
@ -1,6 +1,12 @@
|
||||
# Паспорт прочитал Я выбрал экран таблицей
|
||||
# Годовой план закупок — экран сравнения позиций
|
||||
|
||||
Паспорт прочитал. Я выбрал экран с таблицей, а не конкретную платформу, Alem можно оставить как вашу рекомендацию. Сохранение решений и комментариев нужно сразу, не в будущих версиях. И добавь, что реальные данные обрабатываем только внутри КТ.
|
||||
Паспорт прочитал. Я выбрал экран с таблицей сравнения позиций годового плана закупок, а не конкретную платформу. Alem можно оставить как рекомендацию. Сохранение решений и комментариев нужно сразу, не в будущих версиях. Реальные данные обрабатываем только внутри КТ.
|
||||
|
||||
## Бизнес-процесс
|
||||
|
||||
Сравнение позиций годового плана закупок по группам (ИТ-оборудование, Сетевое оборудование, ПО, Услуги). Каждая позиция имеет характеристики, срок поставки, город, цену, поставщика и оценку совместимости.
|
||||
|
||||
Ответственный сравнивает позиции, проверяет совместимость, исключает несовместимые позиции и принимает решения с комментариями. Массовые действия: согласовать все совместимые, исключить несовместимые. Выгрузка отчёта в Excel.
|
||||
|
||||
## Объём работы
|
||||
|
||||
|
||||
515
index.html
515
index.html
@ -3,12 +3,12 @@
|
||||
<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">
|
||||
<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>
|
||||
.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); }
|
||||
@ -29,10 +29,15 @@
|
||||
.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 .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-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 { 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-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; }
|
||||
@ -42,13 +47,22 @@
|
||||
.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-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-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) {
|
||||
.kt-ai-kpi-strip { grid-template-columns: repeat(2, 1fr); }
|
||||
.kt-ai-filters { flex-direction: column; align-items: stretch; }
|
||||
.kt-ai-filter-group { width: 100%; }
|
||||
.kt-ai-select { width: 100%; }
|
||||
.kt-ai-compare-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -58,7 +72,7 @@
|
||||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||
<div class="kt-ai-header-content">
|
||||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||||
<strong>Система принятия решений</strong>
|
||||
<strong>Годовой план закупок</strong>
|
||||
</div>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<span class="kt-ai-chip" data-tone="orange">Демо-данные</span>
|
||||
@ -70,81 +84,86 @@
|
||||
<section class="kt-ai-kpi-strip">
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-total">0</div>
|
||||
<div class="label">Всего заявок</div>
|
||||
<div class="hint">за выбранный период</div>
|
||||
<div class="label">Всего позиций</div>
|
||||
<div class="hint">в плане закупок</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-review">0</div>
|
||||
<div class="label">На рассмотрении</div>
|
||||
<div class="label">На согласовании</div>
|
||||
<div class="hint">требуют решения</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-approved" style="color: var(--kt-ai-status-ok-fg)">0</div>
|
||||
<div class="label">Принято</div>
|
||||
<div class="hint">за выбранный период</div>
|
||||
<div class="label">Согласовано</div>
|
||||
<div class="hint">включено в план</div>
|
||||
</div>
|
||||
<div class="kt-ai-kpi">
|
||||
<div class="value" id="kpi-rejected" style="color: var(--kt-ai-status-risk-fg)">0</div>
|
||||
<div class="label">Отклонено</div>
|
||||
<div class="hint">за выбранный период</div>
|
||||
<div class="value" id="kpi-excluded" style="color: var(--kt-ai-status-risk-fg)">0</div>
|
||||
<div class="label">Исключено</div>
|
||||
<div class="hint">несовместимые позиции</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="kt-ai-filters">
|
||||
<div class="kt-ai-filter-group">
|
||||
<label class="kt-ai-filter-label">Период</label>
|
||||
<select class="kt-ai-select" id="filter-period" style="min-width: 140px">
|
||||
<option value="7d">7 дней</option>
|
||||
<option value="30d">30 дней</option>
|
||||
<option value="quarter">Квартал</option>
|
||||
<option value="year">Год</option>
|
||||
<label class="kt-ai-filter-label">Группа</label>
|
||||
<select class="kt-ai-select" id="filter-group" style="min-width: 160px">
|
||||
<option value="all">Все группы</option>
|
||||
<option value="it">ИТ-оборудование</option>
|
||||
<option value="network">Сетевое оборудование</option>
|
||||
<option value="software">Программное обеспечение</option>
|
||||
<option value="services">Услуги</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-filter-group">
|
||||
<label class="kt-ai-filter-label">Статус</label>
|
||||
<select class="kt-ai-select" id="filter-status" style="min-width: 160px">
|
||||
<option value="all">Все статусы</option>
|
||||
<option value="new">Новый</option>
|
||||
<option value="review">На рассмотрении</option>
|
||||
<option value="decided">Решение принято</option>
|
||||
<option value="review">На согласовании</option>
|
||||
<option value="approved">Согласовано</option>
|
||||
<option value="rejected">Отклонено</option>
|
||||
<option value="excluded">Исключено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-filter-group">
|
||||
<label class="kt-ai-filter-label">Тип заявки</label>
|
||||
<select class="kt-ai-select" id="filter-type" style="min-width: 160px">
|
||||
<option value="all">Все типы</option>
|
||||
<option value="access">Доступ к данным</option>
|
||||
<option value="integration">Интеграция</option>
|
||||
<option value="hardware">Оборудование</option>
|
||||
<option value="other">Другое</option>
|
||||
<label class="kt-ai-filter-label">Город</label>
|
||||
<select class="kt-ai-select" id="filter-city" style="min-width: 140px">
|
||||
<option value="all">Все города</option>
|
||||
<option value="almaty">Алматы</option>
|
||||
<option value="astana">Астана</option>
|
||||
<option value="shymkent">Шымкент</option>
|
||||
<option value="other">Другие</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="kt-ai-filter-group">
|
||||
<label class="kt-ai-filter-label">Приоритет</label>
|
||||
<select class="kt-ai-select" id="filter-priority" style="min-width: 140px">
|
||||
<option value="all">Все приоритеты</option>
|
||||
<option value="high">Высокий</option>
|
||||
<option value="medium">Средний</option>
|
||||
<option value="low">Низкий</option>
|
||||
<label class="kt-ai-filter-label">Совместимость</label>
|
||||
<select class="kt-ai-select" id="filter-compat" style="min-width: 160px">
|
||||
<option value="all">Все</option>
|
||||
<option value="compatible">Совместима</option>
|
||||
<option value="check">Требует проверки</option>
|
||||
<option value="incompatible">Несовместима</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<button class="kt-ai-btn" data-variant="ghost" id="btn-export" onclick="exportToExcel()">📄 Выгрузить Excel</button>
|
||||
</section>
|
||||
|
||||
<section class="kt-ai-table-wrap">
|
||||
<table class="kt-ai-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Дата</th>
|
||||
<th>Заявитель</th>
|
||||
<th>Тип</th>
|
||||
<th>Приоритет</th>
|
||||
<th style="width: 40px"></th>
|
||||
<th>Наименование</th>
|
||||
<th>Характеристики</th>
|
||||
<th>Срок</th>
|
||||
<th>Город</th>
|
||||
<th class="num">Цена (₸)</th>
|
||||
<th>Совместимость</th>
|
||||
<th>Статус</th>
|
||||
<th>Решение</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="requests-table-body">
|
||||
<tbody id="positions-table-body">
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="empty-state" class="kt-ai-empty" style="display: none">
|
||||
@ -153,24 +172,39 @@
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="kt-ai-modal-overlay" id="modal-overlay">
|
||||
<div class="kt-ai-modal">
|
||||
<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">×</button>
|
||||
</div>
|
||||
<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">
|
||||
<label for="decision-comment">Комментарий к решению</label>
|
||||
<textarea id="decision-comment" class="kt-ai-textarea" rows="3" placeholder="Обоснование решения..."></textarea>
|
||||
</div>
|
||||
|
||||
<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="ghost" id="btn-rework">На доработку</button>
|
||||
<button class="kt-ai-btn" data-variant="ghost" id="btn-exclude">Исключить</button>
|
||||
</div>
|
||||
<p class="kt-ai-decision-note">Решение будет сохранено в localStorage</p>
|
||||
</div>
|
||||
@ -180,53 +214,50 @@
|
||||
<script>
|
||||
// Реестр фильтров
|
||||
const FILTERS = [
|
||||
{ id: 'period', label: 'Период', affects: ['table', 'kpi', 'empty'] },
|
||||
{ id: 'group', label: 'Группа', affects: ['table', 'kpi', 'empty'] },
|
||||
{ id: 'status', label: 'Статус', affects: ['table', 'kpi', 'empty'] },
|
||||
{ id: 'type', label: 'Тип', affects: ['table', 'kpi', 'empty'] },
|
||||
{ id: 'priority', label: 'Приоритет', affects: ['table', 'kpi', 'empty'] }
|
||||
{ id: 'city', label: 'Город', affects: ['table', 'kpi', 'empty'] },
|
||||
{ id: 'compat', label: 'Совместимость', affects: ['table', 'kpi', 'empty'] }
|
||||
];
|
||||
|
||||
// Демо-данные
|
||||
// Демо-данные по группам
|
||||
const DEMO_DATA = {
|
||||
'7d': [
|
||||
{ id: 1, date: '2026-09-10', applicant: 'Иванов А.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'new', 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: 3, date: '2026-09-12', applicant: 'Сидоров Е.В.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Согласовано в рамках бюджета' },
|
||||
{ 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 }
|
||||
'it': [
|
||||
{ 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, 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, 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 }
|
||||
],
|
||||
'30d': [
|
||||
{ id: 6, date: '2026-09-01', applicant: 'Нурсултанов Б.Т.', department: 'Отдел маркетинга', type: 'integration', typeName: 'Интеграция', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция одобрена' },
|
||||
{ id: 7, date: '2026-09-03', applicant: 'Омарова Г.С.', department: 'Департамент ИТ', type: 'access', typeName: 'Доступ к данным', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ предоставлен' },
|
||||
{ id: 8, date: '2026-09-05', applicant: 'Васильев П.И.', department: 'Служба поддержки', type: 'hardware', typeName: 'Оборудование', priority: 'low', priorityName: 'Низкий', status: 'new', 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: 'Согласовано в рамках бюджета' }
|
||||
'network': [
|
||||
{ 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: 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: 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 }
|
||||
],
|
||||
'quarter': [
|
||||
{ id: 14, date: '2026-07-15', applicant: 'Ахметов Р.Б.', department: 'Департамент ИТ', type: 'integration', typeName: 'Интеграция', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Интеграция завершена' },
|
||||
{ id: 15, date: '2026-08-01', applicant: 'Кузнецова А.В.', department: 'Отдел кадров', type: 'access', typeName: 'Доступ к данным', priority: 'medium', priorityName: 'Средний', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Доступ настроен' },
|
||||
{ id: 16, date: '2026-08-10', applicant: 'Токтаев К.С.', department: 'Служба безопасности', type: 'hardware', typeName: 'Оборудование', priority: 'high', priorityName: 'Высокий', status: 'decided', statusName: 'Решение принято', decision: 'approved', decisionName: 'Принято', comment: 'Оборудование закуплено' },
|
||||
{ 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 }
|
||||
'software': [
|
||||
{ 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: 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: 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 }
|
||||
],
|
||||
'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
|
||||
function loadSavedDecisions() {
|
||||
const saved = localStorage.getItem('requestDecisions');
|
||||
const saved = localStorage.getItem('procurementDecisions');
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
}
|
||||
|
||||
// Сохранение решения в localStorage
|
||||
function saveDecision(requestId, decision, comment) {
|
||||
function saveDecision(positionId, decision, comment) {
|
||||
const decisions = loadSavedDecisions();
|
||||
decisions[requestId] = { decision, comment, date: new Date().toISOString() };
|
||||
localStorage.setItem('requestDecisions', JSON.stringify(decisions));
|
||||
decisions[positionId] = { decision, comment, date: new Date().toISOString() };
|
||||
localStorage.setItem('procurementDecisions', JSON.stringify(decisions));
|
||||
}
|
||||
|
||||
// Применение сохранённых решений к демо-данным
|
||||
@ -235,12 +266,21 @@ function applySavedDecisions(data) {
|
||||
return data.map(item => {
|
||||
if (decisions[item.id]) {
|
||||
const saved = decisions[item.id];
|
||||
const statusMap = {
|
||||
'approved': 'approved',
|
||||
'rejected': 'rejected',
|
||||
'excluded': 'excluded'
|
||||
};
|
||||
const statusNameMap = {
|
||||
'approved': 'Согласовано',
|
||||
'rejected': 'Отклонено',
|
||||
'excluded': 'Исключено'
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
decision: saved.decision,
|
||||
decisionName: saved.decision === 'approved' ? 'Принято' : saved.decision === 'rejected' ? 'Отклонено' : 'На доработку',
|
||||
status: 'decided',
|
||||
statusName: 'Решение принято',
|
||||
status: statusMap[saved.decision] || item.status,
|
||||
statusName: statusNameMap[saved.decision] || item.statusName,
|
||||
comment: saved.comment
|
||||
};
|
||||
}
|
||||
@ -248,34 +288,41 @@ function applySavedDecisions(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// Получение данных по фильтру периода
|
||||
function getDataForPeriod(period) {
|
||||
const baseData = DEMO_DATA[period] || [];
|
||||
return applySavedDecisions(baseData);
|
||||
// Получение всех данных
|
||||
function getAllData() {
|
||||
let allData = [];
|
||||
for (const group in DEMO_DATA) {
|
||||
allData = allData.concat(DEMO_DATA[group]);
|
||||
}
|
||||
return applySavedDecisions(allData);
|
||||
}
|
||||
|
||||
// Фильтрация данных
|
||||
function filterData(data, filters) {
|
||||
let filtered = [...data];
|
||||
|
||||
if (filters.group !== 'all') {
|
||||
filtered = filtered.filter(item => item.group === filters.group);
|
||||
}
|
||||
|
||||
if (filters.status !== 'all') {
|
||||
if (filters.status === 'new') {
|
||||
filtered = filtered.filter(item => item.status === 'new');
|
||||
} else if (filters.status === 'review') {
|
||||
if (filters.status === 'review') {
|
||||
filtered = filtered.filter(item => item.status === 'review');
|
||||
} else if (filters.status === 'decided') {
|
||||
filtered = filtered.filter(item => item.status === 'decided' && item.decision === 'approved');
|
||||
} else if (filters.status === 'approved') {
|
||||
filtered = filtered.filter(item => item.status === 'approved');
|
||||
} 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') {
|
||||
filtered = filtered.filter(item => item.type === filters.type);
|
||||
if (filters.city !== 'all') {
|
||||
filtered = filtered.filter(item => item.city === filters.city);
|
||||
}
|
||||
|
||||
if (filters.priority !== 'all') {
|
||||
filtered = filtered.filter(item => item.priority === filters.priority);
|
||||
if (filters.compat !== 'all') {
|
||||
filtered = filtered.filter(item => item.compat === filters.compat);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
@ -285,9 +332,9 @@ function filterData(data, filters) {
|
||||
function calculateKPI(data) {
|
||||
const total = data.length;
|
||||
const review = data.filter(item => item.status === 'review').length;
|
||||
const approved = data.filter(item => item.decision === 'approved').length;
|
||||
const rejected = data.filter(item => item.decision === 'rejected').length;
|
||||
return { total, review, approved, rejected };
|
||||
const approved = data.filter(item => item.status === 'approved').length;
|
||||
const excluded = data.filter(item => item.status === 'excluded').length;
|
||||
return { total, review, approved, excluded };
|
||||
}
|
||||
|
||||
// Отрисовка KPI
|
||||
@ -295,36 +342,50 @@ function renderKPI(kpi) {
|
||||
document.getElementById('kpi-total').textContent = kpi.total;
|
||||
document.getElementById('kpi-review').textContent = kpi.review;
|
||||
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 = {
|
||||
'new': '<span class="kt-ai-chip" data-tone="blue">Новый</span>',
|
||||
'review': '<span class="kt-ai-chip" data-tone="orange">На рассмотрении</span>',
|
||||
'decided': decision === 'approved'
|
||||
? '<span class="kt-ai-chip" data-tone="green">Принято</span>'
|
||||
: decision === 'rejected'
|
||||
? '<span class="kt-ai-chip" data-tone="red">Отклонено</span>'
|
||||
: '<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>',
|
||||
'rejected': '<span class="kt-ai-chip" data-tone="red">Отклонено</span>',
|
||||
'excluded': '<span class="kt-ai-chip" data-tone="red">Исключено</span>'
|
||||
};
|
||||
return chips[status] || chips['new'];
|
||||
return chips[status] || chips['review'];
|
||||
}
|
||||
|
||||
// Цвет приоритета
|
||||
function getPriorityChip(priority) {
|
||||
// Цвет совместимости
|
||||
function getCompatChip(compat) {
|
||||
const chips = {
|
||||
'high': '<span class="kt-ai-chip" data-tone="red">Высокий</span>',
|
||||
'medium': '<span class="kt-ai-chip" data-tone="orange">Средний</span>',
|
||||
'low': '<span class="kt-ai-chip" data-tone="blue">Низкий</span>'
|
||||
'compatible': '<span class="kt-ai-chip" data-tone="green">Совместима</span>',
|
||||
'check': '<span class="kt-ai-chip" data-tone="orange">Требует проверки</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) {
|
||||
const tbody = document.getElementById('requests-table-body');
|
||||
const tbody = document.getElementById('positions-table-body');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
if (data.length === 0) {
|
||||
@ -334,37 +395,71 @@ function renderTable(data) {
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
tbody.innerHTML = data.map(item => `
|
||||
<tr>
|
||||
<td>${item.date}</td>
|
||||
<td>
|
||||
<div style="font-weight: var(--kt-ai-weight-medium)">${item.applicant}</div>
|
||||
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted)">${item.department}</div>
|
||||
</td>
|
||||
<td>${item.typeName}</td>
|
||||
<td>${getPriorityChip(item.priority)}</td>
|
||||
<td>${getStatusChip(item.status, item.decision)}</td>
|
||||
<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 class="num">
|
||||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openModal(${item.id})">Рассмотреть</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
const grouped = groupByCategory(data);
|
||||
|
||||
let html = '';
|
||||
for (const groupKey in grouped) {
|
||||
const items = grouped[groupKey];
|
||||
if (items.length === 0) continue;
|
||||
|
||||
const groupName = items[0].groupName;
|
||||
html += `
|
||||
<tr class="group-row" onclick="toggleGroup('${groupKey}')">
|
||||
<td><span class="group-icon">▼</span></td>
|
||||
<td colspan="8">${groupName} (${items.length} поз.)</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
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 = {
|
||||
period: '7d',
|
||||
group: 'all',
|
||||
status: 'all',
|
||||
type: 'all',
|
||||
priority: 'all'
|
||||
city: 'all',
|
||||
compat: 'all'
|
||||
};
|
||||
|
||||
let currentRequestId = null;
|
||||
let currentPositionId = null;
|
||||
|
||||
// Обновление отображения
|
||||
function updateDisplay() {
|
||||
const data = getDataForPeriod(currentFilters.period);
|
||||
const data = getAllData();
|
||||
const filtered = filterData(data, currentFilters);
|
||||
const kpi = calculateKPI(filtered);
|
||||
|
||||
@ -373,69 +468,155 @@ function updateDisplay() {
|
||||
}
|
||||
|
||||
// Открытие модального окна
|
||||
function openModal(requestId) {
|
||||
const data = getDataForPeriod(currentFilters.period);
|
||||
const request = data.find(item => item.id === requestId);
|
||||
function openModal(positionId) {
|
||||
const data = getAllData();
|
||||
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 = `
|
||||
<div class="kt-ai-info-row">
|
||||
<span class="kt-ai-info-label">Дата</span>
|
||||
<span class="kt-ai-info-value">${request.date}</span>
|
||||
<span class="kt-ai-info-label">Наименование</span>
|
||||
<span class="kt-ai-info-value">${position.name}</span>
|
||||
</div>
|
||||
<div class="kt-ai-info-row">
|
||||
<span class="kt-ai-info-label">Заявитель</span>
|
||||
<span class="kt-ai-info-value">${request.applicant}</span>
|
||||
<span class="kt-ai-info-label">Группа</span>
|
||||
<span class="kt-ai-info-value">${position.groupName}</span>
|
||||
</div>
|
||||
<div class="kt-ai-info-row">
|
||||
<span class="kt-ai-info-label">Отдел</span>
|
||||
<span class="kt-ai-info-value">${request.department}</span>
|
||||
<span class="kt-ai-info-label">Характеристики</span>
|
||||
<span class="kt-ai-info-value">${position.specs}</span>
|
||||
</div>
|
||||
<div class="kt-ai-info-row">
|
||||
<span class="kt-ai-info-label">Тип заявки</span>
|
||||
<span class="kt-ai-info-value">${request.typeName}</span>
|
||||
<span class="kt-ai-info-label">Срок поставки</span>
|
||||
<span class="kt-ai-info-value">${position.deadline}</span>
|
||||
</div>
|
||||
<div class="kt-ai-info-row">
|
||||
<span class="kt-ai-info-label">Приоритет</span>
|
||||
<span class="kt-ai-info-value">${getPriorityChip(request.priority)}</span>
|
||||
<span class="kt-ai-info-label">Город</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 class="kt-ai-info-row">
|
||||
<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>
|
||||
`;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Закрытие модального окна
|
||||
function closeModal() {
|
||||
document.getElementById('modal-overlay').classList.remove('in');
|
||||
currentRequestId = null;
|
||||
currentPositionId = null;
|
||||
}
|
||||
|
||||
// Сохранение решения
|
||||
function makeDecision(decision) {
|
||||
if (!currentRequestId) return;
|
||||
if (!currentPositionId) return;
|
||||
|
||||
const comment = document.getElementById('decision-comment').value.trim();
|
||||
saveDecision(currentRequestId, decision, comment);
|
||||
saveDecision(currentPositionId, decision, comment);
|
||||
|
||||
closeModal();
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
// Показать уведомление
|
||||
alert('Решение сохранено!');
|
||||
// Массовое согласование совместимых
|
||||
function approveAll() {
|
||||
const data = getAllData();
|
||||
const compatible = data.filter(item => item.compat === 'compatible' && item.status === 'review');
|
||||
|
||||
let count = 0;
|
||||
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) => {
|
||||
currentFilters.period = e.target.value;
|
||||
document.getElementById('filter-group').addEventListener('change', (e) => {
|
||||
currentFilters.group = e.target.value;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
@ -444,13 +625,13 @@ document.getElementById('filter-status').addEventListener('change', (e) => {
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
document.getElementById('filter-type').addEventListener('change', (e) => {
|
||||
currentFilters.type = e.target.value;
|
||||
document.getElementById('filter-city').addEventListener('change', (e) => {
|
||||
currentFilters.city = e.target.value;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
document.getElementById('filter-priority').addEventListener('change', (e) => {
|
||||
currentFilters.priority = e.target.value;
|
||||
document.getElementById('filter-compat').addEventListener('change', (e) => {
|
||||
currentFilters.compat = e.target.value;
|
||||
updateDisplay();
|
||||
});
|
||||
|
||||
@ -462,13 +643,17 @@ document.getElementById('modal-overlay').addEventListener('click', (e) => {
|
||||
|
||||
document.getElementById('btn-approve').addEventListener('click', () => makeDecision('approved'));
|
||||
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();
|
||||
|
||||
// Сделать функцию доступной глобально
|
||||
// Сделать функции доступными глобально
|
||||
window.openModal = openModal;
|
||||
window.toggleGroup = toggleGroup;
|
||||
window.approveAll = approveAll;
|
||||
window.excludeIncompatible = excludeIncompatible;
|
||||
window.exportToExcel = exportToExcel;
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
103
passport.md
103
passport.md
@ -1,8 +1,8 @@
|
||||
# Паспорт бизнес-процесса: Система принятия решений по заявкам
|
||||
# Паспорт бизнес-процесса: Согласование годового плана закупок
|
||||
|
||||
## Назначение системы
|
||||
|
||||
Система для обработки и принятия решений по входящим заявкам сотрудников. Позволяет ответственным лицам рассматривать заявки, принимать решения (принято/отклонено), добавлять комментарии и отслеживать статусы.
|
||||
Система для согласования позиций годового плана закупок. Позволяет ответственным лицам сравнивать характеристики позиций, проверять совместимость, исключать неподходящие позиции и принимать решения с комментариями.
|
||||
|
||||
## Важные принципы
|
||||
|
||||
@ -12,70 +12,82 @@
|
||||
|
||||
## Сущности
|
||||
|
||||
### Заявка
|
||||
- **ID** — уникальный номер
|
||||
- **Дата создания** — когда поступила заявка
|
||||
- **Заявитель** — ФИО сотрудника
|
||||
- **Отдел/Департамент** — подразделение заявителя
|
||||
- **Тип заявки** — доступ к данным / интеграция / оборудование / другое
|
||||
- **Описание** — краткое описание потребности
|
||||
- **Приоритет** — низкий / средний / высокий
|
||||
- **Статус** — новый / на рассмотрении / решение принято
|
||||
### Позиция плана закупок
|
||||
- **ID** — уникальный номер позиции
|
||||
- **Группа/Категория** — группа закупок (ИТ-оборудование, Сетевое оборудование, ПО, Услуги)
|
||||
- **Наименование** — название позиции
|
||||
- **Характеристики** — технические параметры (мощность, ёмкость, совместимость)
|
||||
- **Срок поставки** — планируемая дата поставки
|
||||
- **Город** — место доставки
|
||||
- **Цена** — стоимость в тенге
|
||||
- **Количество** — единиц
|
||||
- **Поставщик** — рекомендуемый поставщик
|
||||
- **Совместимость** — совместима / требует проверки / несовместима
|
||||
- **Статус согласования** — на согласовании / согласовано / отклонено / исключено
|
||||
|
||||
### Решение
|
||||
- **ID заявки** — ссылка на заявку
|
||||
- **Решение** — принято / отклонено / на доработку
|
||||
### Решение по позиции
|
||||
- **ID позиции** — ссылка на позицию
|
||||
- **Решение** — согласовано / отклонено / исключено / на доработку
|
||||
- **Комментарий** — обоснование решения
|
||||
- **Кто принял** — ФИО ответственного
|
||||
- **Дата решения** — когда принято решение
|
||||
|
||||
## Статусы заявки
|
||||
## Статусы позиции
|
||||
|
||||
| Статус | Описание | Что можно делать |
|
||||
|--------|----------|------------------|
|
||||
| Новый | Заявка только поступила | Назначить ответственного, взять в работу |
|
||||
| На рассмотрении | Ответственный изучает заявку | Запросить доп. информацию, принять решение |
|
||||
| Решение принято | Решение зафиксировано | Просмотр, экспорт в отчёт |
|
||||
| Отклонено | Заявка отклонена | Просмотр, архивирование |
|
||||
| На согласовании | Позиция ожидает решения | Сравнить, проверить совместимость, принять решение |
|
||||
| Согласовано | Позиция одобрена | Просмотр, включение в итоговый план |
|
||||
| Отклонено | Позиция отклонена | Просмотр, обоснование отклонения |
|
||||
| Исключено | Позиция исключена из плана | Просмотр, причина исключения |
|
||||
|
||||
## Роли пользователей
|
||||
## Группы позиций
|
||||
|
||||
| Роль | Права |
|
||||
|------|-------|
|
||||
| Заявитель | Создать заявку, просмотр своих заявок |
|
||||
| Ответственный | Просмотр назначенных заявок, принятие решений, комментарии |
|
||||
| Администратор | Все заявки, назначение ответственных, экспорт данных |
|
||||
| Группа | Примеры |
|
||||
|--------|---------|
|
||||
| ИТ-оборудование | Серверы, рабочие станции, ноутбуки |
|
||||
| Сетевое оборудование | Маршрутизаторы, коммутаторы, СКС |
|
||||
| Программное обеспечение | Лицензии ПО, антивирусы, ОС |
|
||||
| Услуги | Техническая поддержка, обучение, аутсорсинг |
|
||||
|
||||
## Интерфейс — экран с таблицей
|
||||
## Интерфейс — экран сравнения позиций
|
||||
|
||||
### KPI-полоса (верхняя панель)
|
||||
- Всего заявок
|
||||
- На рассмотрении
|
||||
- Принято за период
|
||||
- Отклонено за период
|
||||
- Всего позиций
|
||||
- На согласовании
|
||||
- Согласовано
|
||||
- Исключено (несовместимые)
|
||||
|
||||
### Фильтры
|
||||
- Период (7 дней / 30 дней / квартал / год)
|
||||
- Статус (все / новый / на рассмотрении / решение принято / отклонено)
|
||||
- Тип заявки (все / доступ / интеграция / оборудование / другое)
|
||||
- Приоритет (все / высокий / средний / низкий)
|
||||
- Группа (все / ИТ-оборудование / Сетевое / ПО / Услуги)
|
||||
- Статус (все / на согласовании / согласовано / отклонено / исключено)
|
||||
- Город (все / Алматы / Астана / Шымкент / другие)
|
||||
- Совместимость (все / совместима / требует проверки / несовместима)
|
||||
|
||||
### Таблица заявок
|
||||
### Таблица позиций (группированная)
|
||||
Колонки:
|
||||
1. Дата
|
||||
2. Заявитель (с отделом)
|
||||
3. Тип заявки
|
||||
4. Приоритет (цветной чип)
|
||||
5. Статус (цветной чип)
|
||||
6. Решение (если принято)
|
||||
7. Действия (кнопка «Рассмотреть»)
|
||||
1. Группа (свёртка/развёртка)
|
||||
2. Наименование
|
||||
3. Характеристики
|
||||
4. Срок поставки
|
||||
5. Город
|
||||
6. Цена (тенге)
|
||||
7. Совместимость (цветной чип)
|
||||
8. Статус (цветной чип)
|
||||
9. Действия (кнопка «Согласовать», «Исключить»)
|
||||
|
||||
### Карточка рассмотрения (открывается по клику)
|
||||
- Полная информация о заявке
|
||||
### Карточка согласования (открывается по клику)
|
||||
- Полная информация о позиции
|
||||
- Сравнение с аналогами (если есть)
|
||||
- Проверка совместимости
|
||||
- Поле для комментария
|
||||
- Кнопки: «Принять» / «Отклонить» / «На доработку»
|
||||
- Кнопки: «Согласовать» / «Отклонить» / «Исключить»
|
||||
- Сохранение в localStorage сразу после нажатия
|
||||
|
||||
### Выгрузка в Excel
|
||||
- Кнопка «Выгрузить отчёт» — генерация Excel-файла с текущими данными
|
||||
- Формат: XLSX, все поля позиции + решения + комментарии
|
||||
|
||||
## Хранение данных
|
||||
|
||||
### Демо-данные
|
||||
@ -97,6 +109,7 @@
|
||||
5. **Пустое состояние** — если нет данных под фильтр, показать причину
|
||||
6. **Фильтры меняют всё** — таблица, KPI, текст объяснения
|
||||
7. **Один пустой срез** — хотя бы один фильтр показывает «нет данных»
|
||||
8. **Группировка строк** — свёртка/развёртка по категориям
|
||||
|
||||
## Запреты
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user