v2: форма по МАЦ Приложению 2

This commit is contained in:
Ahtanova.D 2026-08-21 10:20:20 +00:00
parent 614ba78c99
commit fb0ccc1573
4 changed files with 394 additions and 238 deletions

View File

@ -3,139 +3,203 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Маркетинговый анализ цен</title>
<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: 1200px; margin: 0 auto; padding: 24px; }
.tabs { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
.tab { padding: 10px 20px; border: none; background: white; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 500; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.tab.active { background: #007AFF; color: white; }
.section { display: none; }
.section.active { display: block; }
.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); }
.card h2 { font-size: 20px; margin-bottom: 16px; color: #1d1d1f; }
.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 { width: 100%; padding: 12px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.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; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
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; }
.price-low { background: #fff3cd; }
.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; }
.price-highlight { background: #d4edda; font-weight: 600; }
.empty { text-align: center; padding: 40px; color: #86868b; }
.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; }
@media print {
.btn, .no-print { display: none !important; }
.card { box-shadow: none; border: 1px solid #ddd; }
}
</style>
</head>
<body>
<header>
<h1>📊 Маркетинговый анализ цен</h1>
<p>Сравнение цен казахстанских товаропроизводителей</p>
<h1>📊 МАЦ — Маркетинговый анализ цен</h1>
<p>Форма по Приложению 2 (Самрук-Қазына)</p>
</header>
<div class="container">
<div class="tabs">
<button class="tab active" onclick="showTab('producers')">Производители</button>
<button class="tab" onclick="showTab('products')">Товары и цены</button>
<button class="tab" onclick="showTab('analysis')">Анализ</button>
<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>
<!-- Производители -->
<div id="producers" class="section active">
<div class="card">
<h2>Добавить производителя</h2>
<form id="producerForm">
<div class="form-row">
<div class="form-group">
<label>Название компании</label>
<input type="text" name="name" required placeholder="ТОО «Пример»">
</div>
<div class="form-group">
<label>Город</label>
<input type="text" name="city" required placeholder="Алматы">
</div>
<div class="form-group">
<label>Контакты</label>
<input type="text" name="contact" placeholder="Телефон / сайт">
</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>
<button type="submit" class="btn btn-primary">Добавить производителя</button>
</form>
</div>
<div class="card">
<h2>Список производителей</h2>
<table>
<thead>
<tr><th>Название</th><th>Город</th><th>Контакты</th><th>Действия</th></tr>
</thead>
<tbody id="producersTable"></tbody>
</table>
<div id="producersEmpty" class="empty">Нет производителей</div>
</div>
</div>
<!-- Товары -->
<div id="products" class="section">
<div class="card">
<h2>Добавить товар и цену</h2>
<form id="productForm">
<div class="form-row">
<div class="form-group">
<label>Производитель</label>
<select name="producerId" id="producerSelect" required></select>
</div>
<div class="form-group">
<label>Наименование товара</label>
<input type="text" name="productName" required placeholder="Например: Мука в/с, 50кг">
</div>
<div class="form-group">
<label>Цена (₸)</label>
<input type="number" name="price" required placeholder="15000">
</div>
<div class="form-group">
<label>Ед. измерения</label>
<input type="text" name="unit" placeholder="шт, кг, уп">
</div>
<div class="form-group">
<label>Объем</label>
<input type="text" name="volume" placeholder="с указанием единицы измерения">
</div>
<button type="submit" class="btn btn-success">Добавить товар</button>
</form>
</div>
<div class="card">
<h2>Все товары</h2>
<table>
<thead>
<tr><th>Товар</th><th>Производитель</th><th>Цена</th><th>Ед.</th><th>Действия</th></tr>
</thead>
<tbody id="productsTable"></tbody>
</table>
<div id="productsEmpty" class="empty">Нет товаров</div>
</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>
<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>
<!-- Анализ -->
<div id="analysis" class="section">
<!-- Раздел 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>
<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>
<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="statProducers">0</div>
<div class="stat-label">Производителей</div>
<div class="stat-value" id="statSuppliers">0</div>
<div class="stat-label">Поставщиков</div>
</div>
<div class="stat">
<div class="stat-value" id="statProducts">0</div>
<div class="stat-label">Товаров</div>
<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>
@ -143,185 +207,250 @@
</div>
</div>
<div class="card">
<h2>Сравнение цен по товарам</h2>
<table>
<thead>
<tr><th>Товар</th><th>Мин. цена</th><th>Макс. цена</th><th>Разница</th><th>Лучшее предложение</th></tr>
</thead>
<tbody id="analysisTable"></tbody>
</table>
<div id="analysisEmpty" class="empty">Нет данных для анализа</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="card">
<h2>Экспорт</h2>
<button class="btn btn-primary" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
<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>
<script>
let data = { producers: [], products: [] };
let data = { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false } };
async function loadData() {
const r = await fetch('/api/data');
data = await r.json();
renderProducers();
renderProducts();
if (data.inputData) displayInputData();
renderSuppliers();
renderOffers();
renderAnalysis();
updateProducerSelect();
updateSupplierSelect();
document.getElementById('check1').checked = data.checks.c1;
document.getElementById('check2').checked = data.checks.c2;
document.getElementById('check3').checked = data.checks.c3;
generateConclusion();
}
function renderProducers() {
const tbody = document.getElementById('producersTable');
const empty = document.getElementById('producersEmpty');
if (data.producers.length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
tbody.innerHTML = data.producers.map(p => `
<tr>
<td>${p.name}</td>
<td>${p.city}</td>
<td>${p.contact || '—'}</td>
<td><button class="btn btn-danger" onclick="deleteProducer(${p.id})">Удалить</button></td>
</tr>
`).join('');
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 renderProducts() {
const tbody = document.getElementById('productsTable');
const empty = document.getElementById('productsEmpty');
if (data.products.length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
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.products.map(p => {
const producer = data.producers.find(pr => pr.id === p.producerId);
return `
<tr>
<td>${p.productName}</td>
<td>${producer ? producer.name : '—'}</td>
<td>${Number(p.price).toLocaleString()} ₸</td>
<td>${p.unit || '—'}</td>
<td><button class="btn btn-danger" onclick="deleteProduct(${p.id})">Удалить</button></td>
</tr>
`;
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 byProduct = {};
data.products.forEach(p => {
if (!byProduct[p.productName]) byProduct[p.productName] = [];
const producer = data.producers.find(pr => pr.id === p.producerId);
byProduct[p.productName].push({ price: Number(p.price), producer: producer ? producer.name : '—', unit: p.unit });
});
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;
const tbody = document.getElementById('analysisTable');
const empty = document.getElementById('analysisEmpty');
document.getElementById('statProducers').textContent = data.producers.length;
document.getElementById('statProducts').textContent = data.products.length;
const allPrices = data.products.map(p => Number(p.price));
const avg = allPrices.length ? Math.round(allPrices.reduce((a,b) => a+b, 0) / allPrices.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() + ' ₸';
if (Object.keys(byProduct).length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
tbody.innerHTML = Object.entries(byProduct).map(([name, items]) => {
const prices = items.map(i => i.price);
const min = Math.min(...prices);
const max = Math.max(...prices);
const diff = max - min;
const best = items.find(i => i.price === min);
return `
<tr>
<td>${name}</td>
<td>${min.toLocaleString()} ₸</td>
<td>${max.toLocaleString()} ₸</td>
<td>${diff.toLocaleString()} ₸ (${diff > 0 ? Math.round(diff/min*100) : 0}%)</td>
<td class="price-highlight">${best.producer} — ${best.price.toLocaleString()} ₸/${best.unit || 'шт'}</td>
</tr>
`;
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 updateProducerSelect() {
const sel = document.getElementById('producerSelect');
sel.innerHTML = '<option value="">Выберите производителя</option>' +
data.producers.map(p => `<option value="${p.id}">${p.name} (${p.city})</option>`).join('');
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 showTab(id) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById(id).classList.add('active');
event.target.classList.add('active');
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('producerForm').addEventListener('submit', async (e) => {
document.getElementById('inputDataForm').addEventListener('submit', async (e) => {
e.preventDefault();
const f = e.target;
await fetch('/api/producers', {
await fetch('/api/inputData', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: f.name.value, city: f.city.value, contact: f.contact.value })
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('productForm').addEventListener('submit', async (e) => {
document.getElementById('offerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const f = e.target;
await fetch('/api/products', {
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({
producerId: Number(f.producerId.value),
productName: f.productName.value,
price: Number(f.price.value),
unit: f.unit.value
c1: document.getElementById('check1').checked,
c2: document.getElementById('check2').checked,
c3: document.getElementById('check3').checked
})
});
f.reset();
loadData();
});
async function deleteProducer(id) {
if (!confirm('Удалить производителя и все его товары?')) return;
await fetch('/api/producers/' + id, { method: 'DELETE' });
loadData();
}
async function deleteProduct(id) {
if (!confirm('Удалить товар?')) return;
await fetch('/api/products/' + id, { method: 'DELETE' });
loadData();
alert('Проверка сохранена');
}
function exportToExcel() {
const head = ["Товар", "Производитель", "Город", "Цена", "Ед.изм."];
const body = data.products.map(p => {
const pr = data.producers.find(x => x.id === p.producerId);
return [p.productName, pr ? pr.name : '', pr ? pr.city : '', p.price, p.unit || ''];
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 || '']);
});
const csv = '\uFEFF' + [head.join(';'), ...body.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
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]);
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 = 'analiz_cen.csv';
a.download = 'MAC_' + (data.inputData?.ensCode || 'otchet') + '.csv';
a.click();
}

10
package-lock.json generated Normal file
View File

@ -0,0 +1,10 @@
{
"name": "price-analysis",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "price-analysis"
}
}
}

View File

@ -6,7 +6,7 @@ const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
function readData() {
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { producers: [], products: [] }; }
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { inputData: null, suppliers: [], offers: [], checks: { c1: false, c2: false, c3: false } }; }
}
function writeData(data) {
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
@ -20,49 +20,66 @@ function body(req) {
}
http.createServer(async (req, res) => {
// API: получить все данные
if (req.method === "GET" && req.url === "/api/data") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(readData()));
}
// API: добавить производителя
if (req.method === "POST" && req.url === "/api/producers") {
const p = await body(req);
if (req.method === "POST" && req.url === "/api/inputData") {
const d = await body(req);
const data = readData();
data.producers.push({ id: Date.now(), ...p });
data.inputData = d;
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
// API: добавить товар с ценой
if (req.method === "POST" && req.url === "/api/products") {
const prod = await body(req);
if (req.method === "POST" && req.url === "/api/suppliers") {
const s = await body(req);
const data = readData();
data.products.push({ id: Date.now(), ...prod });
data.suppliers.push({ id: Date.now(), ...s });
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
// API: удалить производителя
if (req.method === "DELETE" && req.url.startsWith("/api/producers/")) {
if (req.method === "POST" && req.url === "/api/offers") {
const o = await body(req);
const data = readData();
data.offers.push({ id: Date.now(), ...o });
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
if (req.method === "POST" && req.url === "/api/checks") {
const c = await body(req);
const data = readData();
data.checks = c;
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
if (req.method === "DELETE" && req.url.startsWith("/api/suppliers/")) {
const id = parseInt(req.url.split("/")[3]);
const data = readData();
data.producers = data.producers.filter(p => p.id !== id);
data.products = data.products.filter(pr => pr.producerId !== id);
data.suppliers = data.suppliers.filter(s => s.id !== id);
data.offers = data.offers.filter(o => o.supplierId !== id);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
// API: удалить товар
if (req.method === "DELETE" && req.url.startsWith("/api/products/")) {
if (req.method === "DELETE" && req.url.startsWith("/api/offers/")) {
const id = parseInt(req.url.split("/")[3]);
const data = readData();
data.products = data.products.filter(p => p.id !== id);
data.offers = data.offers.filter(o => o.id !== id);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
// Статика
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {

Binary file not shown.