332 lines
14 KiB
HTML
332 lines
14 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: 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; }
|
||
.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-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; }
|
||
.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; }
|
||
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; }
|
||
.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; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>📊 Маркетинговый анализ цен</h1>
|
||
<p>Сравнение цен казахстанских товаропроизводителей</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>
|
||
|
||
<!-- Производители -->
|
||
<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>
|
||
</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>
|
||
<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 id="analysis" class="section">
|
||
<div class="stats">
|
||
<div class="stat">
|
||
<div class="stat-value" id="statProducers">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>
|
||
<div class="stat">
|
||
<div class="stat-value" id="statAvgPrice">0</div>
|
||
<div class="stat-label">Средняя цена</div>
|
||
</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>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Экспорт</h2>
|
||
<button class="btn btn-primary" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let data = { producers: [], products: [] };
|
||
|
||
async function loadData() {
|
||
const r = await fetch('/api/data');
|
||
data = await r.json();
|
||
renderProducers();
|
||
renderProducts();
|
||
renderAnalysis();
|
||
updateProducerSelect();
|
||
}
|
||
|
||
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 renderProducts() {
|
||
const tbody = document.getElementById('productsTable');
|
||
const empty = document.getElementById('productsEmpty');
|
||
if (data.products.length === 0) {
|
||
tbody.innerHTML = '';
|
||
empty.style.display = 'block';
|
||
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>
|
||
`;
|
||
}).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 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('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>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
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 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');
|
||
}
|
||
|
||
document.getElementById('producerForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
await fetch('/api/producers', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: f.name.value, city: f.city.value, contact: f.contact.value })
|
||
});
|
||
f.reset();
|
||
loadData();
|
||
});
|
||
|
||
document.getElementById('productForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const f = e.target;
|
||
await fetch('/api/products', {
|
||
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
|
||
})
|
||
});
|
||
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();
|
||
}
|
||
|
||
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 csv = '\uFEFF' + [head.join(';'), ...body.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.click();
|
||
}
|
||
|
||
loadData();
|
||
</script>
|
||
</body>
|
||
</html>
|