584 lines
29 KiB
HTML
584 lines
29 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>МАЦ — Маркетинговый анализ цен</title>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; line-height: 1.5; }
|
||
header { background: linear-gradient(135deg, #007AFF 0%, #00C7BE 100%); color: white; padding: 32px 24px; text-align: center; }
|
||
header h1 { font-size: clamp(24px, 4vw, 36px); margin-bottom: 8px; }
|
||
header p { opacity: 0.9; font-size: 16px; }
|
||
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
||
.section-num { display: inline-block; background: #007AFF; color: white; width: 32px; height: 32px; border-radius: 50%; text-align: center; line-height: 32px; font-weight: 700; margin-right: 12px; }
|
||
.section-title { font-size: 22px; margin-bottom: 20px; color: #1d1d1f; display: flex; align-items: center; }
|
||
.card { background: white; border-radius: 16px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 16px; }
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; font-size: 14px; color: #6e6e73; }
|
||
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 12px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; }
|
||
.form-group textarea { min-height: 100px; resize: vertical; }
|
||
.btn { padding: 12px 24px; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
|
||
.btn-primary { background: #007AFF; color: white; }
|
||
.btn-primary:hover { background: #0056b3; }
|
||
.btn-success { background: #34C759; color: white; }
|
||
.btn-danger { background: #FF3B30; color: white; padding: 8px 16px; font-size: 13px; }
|
||
.btn-secondary { background: #8e8e93; color: white; }
|
||
table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
|
||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e5ea; }
|
||
th { background: #f5f5f7; font-weight: 600; font-size: 13px; color: #6e6e73; text-transform: uppercase; }
|
||
tr:hover { background: #f9f9fb; }
|
||
.checkbox-group { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 16px; padding: 16px; background: #f9f9fb; border-radius: 10px; }
|
||
.checkbox-group input[type="checkbox"] { width: 20px; height: 20px; margin-top: 2px; }
|
||
.checkbox-group label { font-size: 14px; line-height: 1.4; cursor: pointer; }
|
||
.price-highlight { background: #d4edda; font-weight: 600; }
|
||
.empty { text-align: center; padding: 40px; color: #86868b; }
|
||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||
.stat { background: white; padding: 20px; border-radius: 12px; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||
.stat-value { font-size: 32px; font-weight: 700; color: #007AFF; }
|
||
.stat-label { font-size: 13px; color: #6e6e73; margin-top: 4px; }
|
||
.conclusion-box { background: #f0f8ff; border: 2px solid #007AFF; border-radius: 12px; padding: 20px; margin-top: 20px; }
|
||
.signature-block { margin-top: 40px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||
.signature-line { border-top: 1px solid #1d1d1f; padding-top: 8px; margin-top: 40px; }
|
||
.file-list { margin-top: 16px; }
|
||
.file-item { display: flex; align-items: center; justify-content: space-between; padding: 12px; background: #f9f9fb; border-radius: 8px; margin-bottom: 8px; }
|
||
.file-item a { color: #007AFF; text-decoration: none; font-weight: 500; }
|
||
.file-item a:hover { text-decoration: underline; }
|
||
.file-meta { font-size: 12px; color: #86868b; margin-left: 12px; }
|
||
.upload-area { border: 2px dashed #d2d2d7; border-radius: 12px; padding: 24px; text-align: center; background: #fafafa; cursor: pointer; transition: all 0.2s; }
|
||
.upload-area:hover { border-color: #007AFF; background: #f0f8ff; }
|
||
.upload-area.dragover { border-color: #34C759; background: #e8f5e9; }
|
||
.upload-icon { font-size: 48px; margin-bottom: 12px; }
|
||
@media print {
|
||
.btn, .no-print, .upload-area { display: none !important; }
|
||
.card { box-shadow: none; border: 1px solid #ddd; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>📊 МАЦ — Маркетинговый анализ цен</h1>
|
||
<p>Форма по Приложению 2 (Самрук-Қазына)</p>
|
||
</header>
|
||
|
||
<div class="container">
|
||
<div class="no-print" style="text-align: right; margin-bottom: 20px;">
|
||
<button class="btn btn-primary" onclick="window.print()">🖨 Печать / PDF</button>
|
||
<button class="btn btn-success" onclick="exportToExcel()">📤 Выгрузить в Excel</button>
|
||
</div>
|
||
|
||
<!-- Раздел 1: Входные данные -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">1</span>Входные данные</h2>
|
||
<form id="inputDataForm">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Код ЕНС ТРУ</label>
|
||
<input type="text" name="ensCode" placeholder="XX.XX.XX-XXX.X">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Объем</label>
|
||
<input type="text" name="volume" placeholder="с указанием единицы измерения">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Место поставки</label>
|
||
<input type="text" name="deliveryPlace" placeholder="адрес доставки">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Техническая спецификация</label>
|
||
<textarea name="techSpec" placeholder="описание товара/работы/услуги"></textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📎 Прикрепить файлы (ТЗ, спецификации, письма)</label>
|
||
<div class="upload-area" id="uploadArea1" onclick="document.getElementById('fileInput1').click()">
|
||
<div class="upload-icon">📎</div>
|
||
<div>Нажмите для загрузки или перетащите файлы сюда</div>
|
||
<div style="font-size: 12px; color: #86868b; margin-top: 8px;">DOCX, XLSX, PDF до 10 МБ</div>
|
||
</div>
|
||
<input type="file" id="fileInput1" multiple accept=".doc,.docx,.xls,.xlsx,.pdf" style="display: none;" onchange="handleFiles(this.files, 'Входные данные')">
|
||
</div>
|
||
<div class="file-list" id="filesSection1"></div>
|
||
|
||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||
</form>
|
||
<div id="inputDataDisplay" style="margin-top: 20px; padding: 16px; background: #f9f9fb; border-radius: 10px; display: none;"></div>
|
||
</div>
|
||
|
||
<!-- Раздел 2: Список рассылки -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">2</span>Список рассылки и поставщики</h2>
|
||
<form id="supplierForm" style="margin-bottom: 20px;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Наименование компании</label>
|
||
<input type="text" name="name" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Резидент/нерезидент</label>
|
||
<select name="resident" required>
|
||
<option value="Резидент">Резидент РК</option>
|
||
<option value="Нерезидент">Нерезидент</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Статус</label>
|
||
<select name="status" required>
|
||
<option value="Производитель">Производитель</option>
|
||
<option value="Дилер">Дилер</option>
|
||
<option value="Поставщик">Поставщик</option>
|
||
<option value="ТПФ">ТПФ</option>
|
||
<option value="Другое">Другое</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Контактные данные (веб-сайт, телефон, адрес, email)</label>
|
||
<input type="text" name="contacts" placeholder="+7 XXX XXX-XX-XX, example.kz, info@example.kz">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📎 Прикрепить файлы (КП, уставные документы)</label>
|
||
<div class="upload-area" onclick="document.getElementById('fileInput2').click()">
|
||
<div class="upload-icon">📎</div>
|
||
<div>Нажмите для загрузки файлов</div>
|
||
</div>
|
||
<input type="file" id="fileInput2" multiple accept=".doc,.docx,.xls,.xlsx,.pdf" style="display: none;" onchange="handleFiles(this.files, 'Поставщики')">
|
||
</div>
|
||
<div class="file-list" id="filesSection2"></div>
|
||
|
||
<button type="submit" class="btn btn-success">Добавить поставщика</button>
|
||
</form>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th>Компания</th><th>Резидентство</th><th>Статус</th><th>Контакты</th><th class="no-print">Действия</th></tr>
|
||
</thead>
|
||
<tbody id="suppliersTable"></tbody>
|
||
</table>
|
||
<div id="suppliersEmpty" class="empty">Нет поставщиков</div>
|
||
</div>
|
||
|
||
<!-- Раздел 3: Ценовые предложения -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">3</span>Таблица полученных ценовых/коммерческих предложений</h2>
|
||
<form id="offerForm" style="margin-bottom: 20px;">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Поставщик</label>
|
||
<select name="supplierId" id="supplierSelect" required></select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Номер запроса</label>
|
||
<input type="text" name="requestNumber" placeholder="№ письма/запроса">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Цена за ед. (тенге, без НДС)</label>
|
||
<input type="number" name="price" required placeholder="15000">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Дата получения</label>
|
||
<input type="date" name="date">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📎 Прикрепить файлы (КП, письма)</label>
|
||
<div class="upload-area" onclick="document.getElementById('fileInput3').click()">
|
||
<div class="upload-icon">📎</div>
|
||
<div>Нажмите для загрузки файлов</div>
|
||
</div>
|
||
<input type="file" id="fileInput3" multiple accept=".doc,.docx,.xls,.xlsx,.pdf" style="display: none;" onchange="handleFiles(this.files, 'Предложения')">
|
||
</div>
|
||
<div class="file-list" id="filesSection3"></div>
|
||
|
||
<button type="submit" class="btn btn-success">Добавить предложение</button>
|
||
</form>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th>Поставщик</th><th>№ запроса</th><th>Цена (тенге)</th><th>Дата</th><th class="no-print">Действия</th></tr>
|
||
</thead>
|
||
<tbody id="offersTable"></tbody>
|
||
</table>
|
||
<div id="offersEmpty" class="empty">Нет предложений</div>
|
||
</div>
|
||
|
||
<!-- Раздел 4: Проверка по статье 31 -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">4</span>Заключение по проверке (ст. 31 Порядка закупок)</h2>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="check1">
|
||
<label for="check1">Потенциальный поставщик или его субподрядчик НЕ состоит в реестре недобросовестных участников</label>
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="check2">
|
||
<label for="check2">НЕ включены в перечень организаций, связанных с финансированием терроризма</label>
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<input type="checkbox" id="check3">
|
||
<label for="check3">НЕ является юридическим лицом из государства с льготным налогообложением</label>
|
||
</div>
|
||
<button class="btn btn-primary no-print" onclick="saveChecks()">Сохранить проверку</button>
|
||
</div>
|
||
|
||
<!-- Раздел 5: Расчеты и анализ -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">5</span>Расчеты и анализ</h2>
|
||
<div class="stats">
|
||
<div class="stat">
|
||
<div class="stat-value" id="statSuppliers">0</div>
|
||
<div class="stat-label">Поставщиков</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="statOffers">0</div>
|
||
<div class="stat-label">Предложений</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="statMinPrice">0</div>
|
||
<div class="stat-label">Мин. цена</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="statMaxPrice">0</div>
|
||
<div class="stat-label">Макс. цена</div>
|
||
</div>
|
||
<div class="stat">
|
||
<div class="stat-value" id="statAvgPrice">0</div>
|
||
<div class="stat-label">Средняя цена</div>
|
||
</div>
|
||
</div>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr><th>Поставщик</th><th>Цена за ед. (тенге)</th><th>Маркетинговая цена</th><th>Отклонение</th></tr>
|
||
</thead>
|
||
<tbody id="analysisTable"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Раздел 6: Заключение -->
|
||
<div class="card">
|
||
<h2 class="section-title"><span class="section-num">6</span>Заключение с выводами и рекомендациями</h2>
|
||
<div class="conclusion-box">
|
||
<p id="conclusionText" style="white-space: pre-line; line-height: 1.8;"></p>
|
||
<p style="margin-top: 20px; font-style: italic; color: #6e6e73;">На основании цен потенциальных поставщиков/производителей определены маркетинговые цены без учета НДС (КП представлены).</p>
|
||
</div>
|
||
|
||
<div class="signature-block">
|
||
<div>
|
||
<p><strong>Инициатор потребности:</strong></p>
|
||
<div class="signature-line">__________________ / _________________</div>
|
||
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||
</div>
|
||
<div>
|
||
<p><strong>Исполнитель:</strong></p>
|
||
<div class="signature-line">__________________ / _________________</div>
|
||
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top: 40px;">
|
||
<p><strong>Утверждено:</strong></p>
|
||
<div class="signature-line">__________________ / _________________</div>
|
||
<p style="font-size: 13px; color: #6e6e73; margin-top: 8px;">(подпись) (ФИО)</p>
|
||
<p style="margin-top: 16px;">«___» __________ 2026 г.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Все файлы -->
|
||
<div class="card">
|
||
<h2 class="section-title">📎 Все прикреплённые файлы</h2>
|
||
<div class="file-list" id="allFilesList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let data = { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false }, files: [] };
|
||
|
||
async function loadData() {
|
||
const r = await fetch('/api/data');
|
||
data = await r.json();
|
||
if (data.inputData) displayInputData();
|
||
renderSuppliers();
|
||
renderOffers();
|
||
renderAnalysis();
|
||
updateSupplierSelect();
|
||
document.getElementById('check1').checked = data.checks.c1;
|
||
document.getElementById('check2').checked = data.checks.c2;
|
||
document.getElementById('check3').checked = data.checks.c3;
|
||
renderFiles();
|
||
generateConclusion();
|
||
}
|
||
|
||
function displayInputData() {
|
||
const d = data.inputData;
|
||
document.getElementById('inputDataDisplay').innerHTML =
|
||
'<strong>Код ЕНС:</strong> ' + (d.ensCode || '—') +
|
||
' | <strong>Объем:</strong> ' + (d.volume || '—') +
|
||
' | <strong>Место поставки:</strong> ' + (d.deliveryPlace || '—') +
|
||
(d.techSpec ? '<br><strong>Спецификация:</strong> ' + d.techSpec : '');
|
||
document.getElementById('inputDataDisplay').style.display = 'block';
|
||
}
|
||
|
||
function renderSuppliers() {
|
||
const tbody = document.getElementById('suppliersTable');
|
||
const empty = document.getElementById('suppliersEmpty');
|
||
if (data.suppliers.length === 0) { empty.style.display = 'block'; tbody.innerHTML = ''; return; }
|
||
empty.style.display = 'none';
|
||
tbody.innerHTML = data.suppliers.map(s =>
|
||
'<tr><td>' + s.name + '</td><td>' + s.resident + '</td><td>' + s.status + '</td><td>' + (s.contacts || '—') +
|
||
'</td><td class="no-print"><button class="btn btn-danger" onclick="deleteSupplier(' + s.id + ')">Удалить</button></td></tr>'
|
||
).join('');
|
||
}
|
||
|
||
function renderOffers() {
|
||
const tbody = document.getElementById('offersTable');
|
||
const empty = document.getElementById('offersEmpty');
|
||
if (data.offers.length === 0) { empty.style.display = 'block'; tbody.innerHTML = ''; return; }
|
||
empty.style.display = 'none';
|
||
tbody.innerHTML = data.offers.map(o => {
|
||
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||
return '<tr><td>' + (s ? s.name : '—') + '</td><td>' + (o.requestNumber || '—') +
|
||
'</td><td>' + Number(o.price).toLocaleString() + '</td><td>' + (o.date || '—') +
|
||
'</td><td class="no-print"><button class="btn btn-danger" onclick="deleteOffer(' + o.id + ')">Удалить</button></td></tr>';
|
||
}).join('');
|
||
}
|
||
|
||
function renderAnalysis() {
|
||
const prices = data.offers.map(o => Number(o.price));
|
||
const min = prices.length ? Math.min(...prices) : 0;
|
||
const max = prices.length ? Math.max(...prices) : 0;
|
||
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||
|
||
document.getElementById('statSuppliers').textContent = data.suppliers.length;
|
||
document.getElementById('statOffers').textContent = data.offers.length;
|
||
document.getElementById('statMinPrice').textContent = min.toLocaleString() + ' ₸';
|
||
document.getElementById('statMaxPrice').textContent = max.toLocaleString() + ' ₸';
|
||
document.getElementById('statAvgPrice').textContent = avg.toLocaleString() + ' ₸';
|
||
|
||
const tbody = document.getElementById('analysisTable');
|
||
tbody.innerHTML = data.offers.map(o => {
|
||
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||
const p = Number(o.price);
|
||
const dev = min > 0 ? ((p - min) / min * 100).toFixed(1) : 0;
|
||
const isMin = p === min && min > 0;
|
||
return '<tr' + (isMin ? ' class="price-highlight"' : '') +
|
||
'><td>' + (s ? s.name : '—') + '</td><td>' + p.toLocaleString() +
|
||
'</td><td>' + (isMin ? min.toLocaleString() + ' ✓' : '—') +
|
||
'</td><td>' + (dev > 0 ? '+' + dev + '%' : '0%') + '</td></tr>';
|
||
}).join('');
|
||
|
||
generateConclusion(min, avg);
|
||
}
|
||
|
||
function renderFiles() {
|
||
const allFiles = document.getElementById('allFilesList');
|
||
const section1 = document.getElementById('filesSection1');
|
||
const section2 = document.getElementById('filesSection2');
|
||
const section3 = document.getElementById('filesSection3');
|
||
|
||
const inputFiles = data.files.filter(f => f.section === 'Входные данные');
|
||
const supplierFiles = data.files.filter(f => f.section === 'Поставщики');
|
||
const offerFiles = data.files.filter(f => f.section === 'Предложения');
|
||
|
||
function renderFileList(files, container) {
|
||
if (files.length === 0) { container.innerHTML = ''; return; }
|
||
container.innerHTML = files.map(f =>
|
||
'<div class="file-item">' +
|
||
'<a href="' + f.path + '" download>📄 ' + f.name + '</a>' +
|
||
'<span class="file-meta">' + new Date(f.date).toLocaleDateString() + '</span>' +
|
||
'<button class="btn btn-danger no-print" onclick="deleteFile(\'' + f.path.split('/').pop() + '\')">✕</button>' +
|
||
'</div>'
|
||
).join('');
|
||
}
|
||
|
||
renderFileList(inputFiles, section1);
|
||
renderFileList(supplierFiles, section2);
|
||
renderFileList(offerFiles, section3);
|
||
renderFileList(data.files, allFiles);
|
||
}
|
||
|
||
async function handleFiles(files, section) {
|
||
if (!files.length) return;
|
||
const formData = new FormData();
|
||
for (const f of files) {
|
||
formData.append('file', f);
|
||
}
|
||
formData.append('section', section);
|
||
|
||
const r = await fetch('/api/upload', { method: 'POST', body: formData });
|
||
const result = await r.json();
|
||
if (result.ok) {
|
||
loadData();
|
||
}
|
||
}
|
||
|
||
async function deleteFile(fileName) {
|
||
if (!confirm('Удалить файл?')) return;
|
||
await fetch('/api/files/' + fileName, { method: 'DELETE' });
|
||
loadData();
|
||
}
|
||
|
||
function generateConclusion(min, avg) {
|
||
const minOffer = data.offers.find(o => Number(o.price) === min);
|
||
const s = minOffer ? data.suppliers.find(x => x.id === minOffer.supplierId) : null;
|
||
const allChecked = data.checks.c1 && data.checks.c2 && data.checks.c3;
|
||
|
||
let text = 'На основании проведенного маркетингового анализа цен:\n\n';
|
||
text += '1. Запрошено коммерческих предложений: ' + data.offers.length + ' шт.\n';
|
||
text += '2. Количество потенциальных поставщиков: ' + data.suppliers.length + ' ед.\n';
|
||
text += '3. Диапазон цен: от ' + (min || 0).toLocaleString() + ' до ' + (avg ? Math.max(...data.offers.map(o => Number(o.price))) : 0).toLocaleString() + ' тенге без НДС.\n';
|
||
text += '4. Средняя рыночная цена: ' + (avg || 0).toLocaleString() + ' тенге без НДС.\n\n';
|
||
|
||
if (minOffer && s) {
|
||
text += 'РЕКОМЕНДАЦИЯ: Наиболее выгодное предложение от ' + s.name + ' (' + s.status.toLowerCase() + ', ' + s.resident.toLowerCase() + ') по цене ' + min.toLocaleString() + ' тенге.\n\n';
|
||
}
|
||
|
||
if (allChecked) {
|
||
text += 'ПРОВЕРКА ПО СТ. 31: Все пункты подтверждены — поставщик соответствует требованиям Порядка закупок.\n';
|
||
} else {
|
||
text += 'ПРОВЕРКА ПО СТ. 31: Требуется проверка отдельных пунктов.\n';
|
||
}
|
||
|
||
document.getElementById('conclusionText').textContent = text;
|
||
}
|
||
|
||
function updateSupplierSelect() {
|
||
document.getElementById('supplierSelect').innerHTML = '<option value="">Выберите поставщика</option>' +
|
||
data.suppliers.map(s => '<option value="' + s.id + '">' + s.name + ' (' + s.status + ')</option>').join('');
|
||
}
|
||
|
||
document.getElementById('inputDataForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
await fetch('/api/inputData', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ ensCode: f.ensCode.value, volume: f.volume.value, deliveryPlace: f.deliveryPlace.value, techSpec: f.techSpec.value })
|
||
});
|
||
loadData();
|
||
});
|
||
|
||
document.getElementById('supplierForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
await fetch('/api/suppliers', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: f.name.value, resident: f.resident.value, status: f.status.value, contacts: f.contacts.value })
|
||
});
|
||
f.reset();
|
||
loadData();
|
||
});
|
||
|
||
document.getElementById('offerForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
await fetch('/api/offers', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ supplierId: Number(f.supplierId.value), requestNumber: f.requestNumber.value, price: Number(f.price.value), date: f.date.value })
|
||
});
|
||
f.reset();
|
||
loadData();
|
||
});
|
||
|
||
async function deleteSupplier(id) {
|
||
if (!confirm('Удалить поставщика и все его предложения?')) return;
|
||
await fetch('/api/suppliers/' + id, { method: 'DELETE' });
|
||
loadData();
|
||
}
|
||
|
||
async function deleteOffer(id) {
|
||
if (!confirm('Удалить предложение?')) return;
|
||
await fetch('/api/offers/' + id, { method: 'DELETE' });
|
||
loadData();
|
||
}
|
||
|
||
async function saveChecks() {
|
||
await fetch('/api/checks', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
c1: document.getElementById('check1').checked,
|
||
c2: document.getElementById('check2').checked,
|
||
c3: document.getElementById('check3').checked
|
||
})
|
||
});
|
||
loadData();
|
||
alert('Проверка сохранена');
|
||
}
|
||
|
||
function exportToExcel() {
|
||
const head = ["Раздел", "Параметр", "Значение"];
|
||
const rows = [];
|
||
|
||
if (data.inputData) {
|
||
rows.push(["1. Входные данные", "Код ЕНС ТРУ", data.inputData.ensCode || '']);
|
||
rows.push(["1. Входные данные", "Объем", data.inputData.volume || '']);
|
||
rows.push(["1. Входные данные", "Место поставки", data.inputData.deliveryPlace || '']);
|
||
rows.push(["1. Входные данные", "Техническая спецификация", data.inputData.techSpec || '']);
|
||
}
|
||
|
||
data.suppliers.forEach((s, i) => {
|
||
rows.push(["2. Поставщики", "Компания " + (i+1), s.name]);
|
||
rows.push(["2. Поставщики", "Резидентство", s.resident]);
|
||
rows.push(["2. Поставщики", "Статус", s.status]);
|
||
rows.push(["2. Поставщики", "Контакты", s.contacts || '']);
|
||
});
|
||
|
||
data.offers.forEach((o, i) => {
|
||
const s = data.suppliers.find(x => x.id === o.supplierId);
|
||
rows.push(["3. Предложения", "Предложение " + (i+1) + " - " + (s ? s.name : ''), o.price + ' тенге']);
|
||
});
|
||
|
||
rows.push(["4. Проверка ст.31", "Пункт 1", data.checks.c1 ? 'Подтверждено' : 'Не проверено']);
|
||
rows.push(["4. Проверка ст.31", "Пункт 2", data.checks.c2 ? 'Подтверждено' : 'Не проверено']);
|
||
rows.push(["4. Проверка ст.31", "Пункт 3", data.checks.c3 ? 'Подтверждено' : 'Не проверено']);
|
||
|
||
const prices = data.offers.map(o => Number(o.price));
|
||
const min = prices.length ? Math.min(...prices) : 0;
|
||
const avg = prices.length ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0;
|
||
rows.push(["5. Анализ", "Минимальная цена", min.toLocaleString() + ' тенге']);
|
||
rows.push(["5. Анализ", "Средняя цена", avg.toLocaleString() + ' тенге']);
|
||
rows.push(["5. Анализ", "Количество предложений", data.offers.length]);
|
||
|
||
data.files.forEach(f => {
|
||
rows.push(["Файлы", f.name, f.section]);
|
||
});
|
||
|
||
const csv = '\uFEFF' + [head.join(';'), ...rows.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
|
||
a.download = 'MAC_' + (data.inputData?.ensCode || 'otchet') + '.csv';
|
||
a.click();
|
||
}
|
||
|
||
const uploadAreas = document.querySelectorAll('.upload-area');
|
||
uploadAreas.forEach(area => {
|
||
area.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
area.classList.add('dragover');
|
||
});
|
||
area.addEventListener('dragleave', () => {
|
||
area.classList.remove('dragover');
|
||
});
|
||
area.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
area.classList.remove('dragover');
|
||
const input = area.parentElement.querySelector('input[type="file"]');
|
||
if (input) {
|
||
input.files = e.dataTransfer.files;
|
||
const event = new Event('change');
|
||
input.dispatchEvent(event);
|
||
}
|
||
});
|
||
});
|
||
|
||
loadData();
|
||
</script>
|
||
</body>
|
||
</html>
|