nado-sozdat-po-marketingovom/index.html

461 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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; }
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; }
.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>Форма по Приложению 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>
<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>
<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="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>
<script>
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();
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;
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 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]);
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();
}
loadData();
</script>
</body>
</html>