dashbord-prodazh-po-regionam/index.html

311 lines
12 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" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<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">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
.kpi-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-8); }
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-3xl); padding: var(--kt-ai-space-6); box-shadow: var(--kt-ai-card-shadow); }
.kpi-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); line-height: 1; margin-bottom: var(--kt-ai-space-2); }
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
.kpi-hint { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-1); }
.chart-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-3xl); padding: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-8); }
.table-wrap { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-3xl); overflow: hidden; }
.toolbar { display: flex; gap: var(--kt-ai-space-4); align-items: center; flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.status-pill { display: inline-block; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-ok { background: var(--kt-ai-status-ok-bg); color: var(--kt-ai-status-ok-fg); }
.status-warn { background: var(--kt-ai-status-warn-bg); color: var(--kt-ai-status-warn-fg); }
.status-risk { background: var(--kt-ai-status-risk-bg); color: var(--kt-ai-status-risk-fg); }
.region-color { width: 12px; height: 12px; border-radius: 3px; display: inline-block; vertical-align: middle; margin-right: 8px; }
a.kt-ai-btn { text-decoration: none; }
.page-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.page-title h1 { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-semibold); margin: 0; }
.page-title p { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin: 4px 0 0; }
@media (max-width: 640px) {
.kpi-strip { grid-template-columns: 1fr; }
.toolbar { flex-direction: column; align-items: stretch; }
.toolbar > * { width: 100%; }
}
</style>
</head>
<body>
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<strong>Дашборд продаж</strong>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="refreshBtn">
<svg class="kt-icon kt-icon-sm" style="margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#refreshCw"></use></svg>
Обновить
</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding-top:var(--kt-ai-space-9);padding-bottom:var(--kt-ai-space-9)">
<div class="page-header">
<div class="page-title">
<h1>Продажи по регионам</h1>
<p>Сводные показатели за выбранный период</p>
</div>
<div class="toolbar" style="margin:0">
<div class="kt-ai-field" style="margin:0;min-width:180px">
<label class="kt-ai-label" style="margin-bottom:4px">Период</label>
<select class="kt-ai-select" id="monthSelect">
<option value="0">Текущий месяц</option>
<option value="1">Прошлый месяц</option>
<option value="2">2 месяца назад</option>
<option value="3">3 месяца назад</option>
</select>
</div>
</div>
</div>
<div class="kpi-strip" id="kpiStrip">
<div class="kpi-card">
<div class="kpi-value" id="kpiRevenue">0 ₸</div>
<div class="kpi-label">Общая выручка</div>
<div class="kpi-hint" id="kpiRevenueHint">за период</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiPlan">0%</div>
<div class="kpi-label">Выполнение плана</div>
<div class="kpi-hint" id="kpiPlanHint">среднее по регионам</div>
</div>
<div class="kpi-card">
<div class="kpi-value" id="kpiDeals">0</div>
<div class="kpi-label">Количество сделок</div>
<div class="kpi-hint" id="kpiDealsHint">за период</div>
</div>
</div>
<div class="chart-wrap">
<canvas id="salesChart" height="80"></canvas>
</div>
<div class="table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th class="kt-ai-sticky-first">Регион</th>
<th style="text-align:right">План</th>
<th style="text-align:right">Факт</th>
<th style="text-align:right">%</th>
<th>Статус</th>
</tr>
</thead>
<tbody id="regionsTable"></tbody>
</table>
</div>
</main>
<script>
const REGIONS = [
{ id: 'almaty', name: 'Алматы', color: '#0096d7' },
{ id: 'astana', name: 'Астана', color: '#4b9ce2' },
{ id: 'shymkent', name: 'Шымкент', color: '#7fc794' },
{ id: 'zko', name: 'ЗКО', color: '#f5a35f' },
{ id: 'vko', name: 'ВКО', color: '#bb9ae0' },
{ id: 'sko', name: 'СКО', color: '#f0928a' },
{ id: 'ako', name: 'АКО', color: '#1683f3' },
{ id: 'zho', name: 'Жамбылская обл.', color: '#34a853' },
{ id: 'kzo', name: 'Кызылординская обл.', color: '#ff6a00' },
{ id: 'ako2', name: 'Актюбинская обл.', color: '#d2785f' },
{ id: 'mangistau', name: 'Мангистауская обл.', color: '#ea2f6b' },
{ id: 'atyrau', name: 'Атырауская обл.', color: '#ef4444' },
{ id: 'pavlodar', name: 'Павлодарская обл.', color: '#8a5fb0' },
{ id: 'karaganda', name: 'Карагандинская обл.', color: '#0284c7' },
{ id: 'kostanay', name: 'Костанайская обл.', color: '#16a34a' },
{ id: 'turkestan', name: 'Туркестанская обл.', color: '#f97316' }
];
const MONTH_NAMES = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'];
function generateData(monthOffset) {
const now = new Date();
const targetMonth = new Date(now.getFullYear(), now.getMonth() - monthOffset, 1);
const data = [];
let totalRevenue = 0;
let totalPlan = 0;
let totalActual = 0;
let totalDeals = 0;
REGIONS.forEach((region, idx) => {
const basePlan = 50 + Math.random() * 150;
const plan = Math.round(basePlan * 1e6) * 1e6;
const performance = 0.75 + Math.random() * 0.35;
const actual = Math.round(plan * performance);
const deals = Math.floor(15 + Math.random() * 85);
totalRevenue += actual;
totalPlan += plan;
totalActual += actual;
totalDeals += deals;
data.push({
region: region.name,
color: region.color,
plan: plan,
actual: actual,
percent: Math.round(performance * 100),
deals: deals,
status: performance >= 1 ? 'ok' : performance >= 0.9 ? 'warn' : 'risk'
});
});
return {
data,
kpi: {
revenue: totalRevenue,
planPercent: Math.round((totalActual / totalPlan) * 100),
deals: totalDeals
}
};
}
function formatMoney(num) {
if (num >= 1e9) return (num / 1e9).toFixed(1) + ' млрд ₸';
if (num >= 1e6) return (num / 1e6).toFixed(0) + ' млн ₸';
return num.toLocaleString('ru-RU') + ' ₸';
}
function getStatusClass(status) {
return status === 'ok' ? 'status-ok' : status === 'warn' ? 'status-warn' : 'status-risk';
}
function getStatusText(status) {
return status === 'ok' ? 'Выполнен' : status === 'warn' ? 'В работе' : 'Отставание';
}
let chart = null;
function renderChart(data) {
const ctx = document.getElementById('salesChart').getContext('2d');
if (chart) chart.destroy();
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(d => d.region),
datasets: [{
label: 'Продажи',
data: data.map(d => d.actual),
backgroundColor: data.map(d => d.color),
borderRadius: 4,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return formatMoney(context.raw);
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
if (value >= 1e9) return (value / 1e9).toFixed(0) + ' млрд';
if (value >= 1e6) return (value / 1e6).toFixed(0) + ' млн';
return value;
}
}
},
x: {
ticks: { maxRotation: 45, minRotation: 45 }
}
}
}
});
}
function renderTable(data) {
const tbody = document.getElementById('regionsTable');
tbody.innerHTML = data.map(d => `
<tr>
<td class="kt-ai-sticky-first">
<span class="region-color" style="background:${d.color}"></span>
${d.region}
</td>
<td style="text-align:right">${formatMoney(d.plan)}</td>
<td style="text-align:right">${formatMoney(d.actual)}</td>
<td style="text-align:right;font-weight:600">${d.percent}%</td>
<td><span class="status-pill ${getStatusClass(d.status)}">${getStatusText(d.status)}</span></td>
</tr>
`).join('');
}
function renderKPI(kpi) {
document.getElementById('kpiRevenue').textContent = formatMoney(kpi.revenue);
document.getElementById('kpiPlan').textContent = kpi.planPercent + '%';
document.getElementById('kpiDeals').textContent = kpi.deals;
}
function loadData() {
const monthOffset = parseInt(document.getElementById('monthSelect').value) || 0;
const storageKey = 'dashboard_data_' + monthOffset;
let cached = localStorage.getItem(storageKey);
if (cached) {
try {
const parsed = JSON.parse(cached);
return parsed;
} catch (e) {}
}
const fresh = generateData(monthOffset);
localStorage.setItem(storageKey, JSON.stringify(fresh));
return fresh;
}
function refresh() {
const monthOffset = parseInt(document.getElementById('monthSelect').value) || 0;
const storageKey = 'dashboard_data_' + monthOffset;
localStorage.removeItem(storageKey);
const btn = document.getElementById('refreshBtn');
const original = btn.innerHTML;
btn.innerHTML = '<svg class="kt-icon kt-icon-sm" style="margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#loader"></use></svg> Загрузка...';
setTimeout(() => {
const data = loadData();
renderChart(data.data);
renderTable(data.data);
renderKPI(data.kpi);
btn.innerHTML = original;
}, 600);
}
document.getElementById('monthSelect').addEventListener('change', function() {
const data = loadData();
renderChart(data.data);
renderTable(data.data);
renderKPI(data.kpi);
});
document.getElementById('refreshBtn').addEventListener('click', refresh);
const data = loadData();
renderChart(data.data);
renderTable(data.data);
renderKPI(data.kpi);
</script>
</body>
</html>