964 lines
27 KiB
HTML
964 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Дашборд продаж автомобилей - Казахстан</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 24px 32px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
h1 {
|
||
color: #1a202c;
|
||
font-size: 32px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #718096;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.kpi-card {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.kpi-card:hover {
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.kpi-label {
|
||
color: #718096;
|
||
font-size: 14px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.kpi-value {
|
||
color: #1a202c;
|
||
font-size: 36px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.kpi-change {
|
||
color: #48bb78;
|
||
font-size: 14px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.filters {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.filters-title {
|
||
color: #1a202c;
|
||
font-size: 18px;
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.filter-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.filter-group label {
|
||
color: #4a5568;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.filter-group select,
|
||
.filter-group input {
|
||
padding: 10px 16px;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
transition: border-color 0.2s;
|
||
min-width: 180px;
|
||
}
|
||
|
||
.filter-group select:focus,
|
||
.filter-group input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: 10px 24px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
align-self: flex-end;
|
||
}
|
||
|
||
.filter-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.filter-btn.reset {
|
||
background: #e2e8f0;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.charts-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.chart-card {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.chart-title {
|
||
color: #1a202c;
|
||
font-size: 18px;
|
||
margin-bottom: 20px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.chart-container {
|
||
position: relative;
|
||
height: 300px;
|
||
}
|
||
|
||
.table-section {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16px;
|
||
padding: 24px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.table-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
|
||
.table-title {
|
||
color: #1a202c;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
}
|
||
|
||
.search-box input {
|
||
padding: 10px 16px 10px 40px;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
width: 300px;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.search-box input:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.search-box::before {
|
||
content: "🔍";
|
||
position: absolute;
|
||
left: 12px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
overflow-x: auto;
|
||
display: block;
|
||
}
|
||
|
||
.data-table thead {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.data-table th,
|
||
.data-table td {
|
||
padding: 14px 16px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.data-table th {
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.data-table tbody tr:hover {
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.data-table tbody tr:nth-child(even) {
|
||
background: #f7fafc;
|
||
}
|
||
|
||
.data-table tbody tr:nth-child(even):hover {
|
||
background: #edf2f7;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge-standard {
|
||
background: #e2e8f0;
|
||
color: #4a5568;
|
||
}
|
||
|
||
.badge-comfort {
|
||
background: #bee3f8;
|
||
color: #2b6cb0;
|
||
}
|
||
|
||
.badge-premium {
|
||
background: #fef5e7;
|
||
color: #c05621;
|
||
}
|
||
|
||
.color-dot {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.color-white { background: #ffffff; }
|
||
.color-black { background: #1a202c; }
|
||
.color-silver { background: #cbd5e0; }
|
||
|
||
.export-btn {
|
||
padding: 10px 20px;
|
||
background: #48bb78;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.export-btn:hover {
|
||
background: #38a169;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.pagination button {
|
||
padding: 8px 16px;
|
||
border: 2px solid #e2e8f0;
|
||
background: white;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.pagination button:hover:not(:disabled) {
|
||
border-color: #667eea;
|
||
color: #667eea;
|
||
}
|
||
|
||
.pagination button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pagination span {
|
||
color: #4a5568;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 60px;
|
||
color: white;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.charts-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.filter-row {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.filter-group select,
|
||
.filter-group input {
|
||
width: 100%;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>📊 Дашборд продаж автомобилей</h1>
|
||
<p class="subtitle">Kaisar KZ-7 · Казахстан · <span id="periodLabel">последние 30 дней</span></p>
|
||
</header>
|
||
|
||
<div class="kpi-grid" id="kpiGrid">
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Всего продано авто</div>
|
||
<div class="kpi-value" id="totalUnits">0</div>
|
||
<div class="kpi-change">🚗 единиц</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Общая выручка</div>
|
||
<div class="kpi-value" id="totalRevenue">0</div>
|
||
<div class="kpi-change">💰 тенге</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Средний чек</div>
|
||
<div class="kpi-value" id="avgCheck">0</div>
|
||
<div class="kpi-change">📈 тенге</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-label">Регионов охвачено</div>
|
||
<div class="kpi-value" id="totalRegions">0</div>
|
||
<div class="kpi-change">📍 Казахстан</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filters">
|
||
<div class="filters-title">🔧 Фильтры данных</div>
|
||
<div class="filter-row">
|
||
<div class="filter-group">
|
||
<label>Регион</label>
|
||
<select id="regionFilter">
|
||
<option value="all">Все регионы</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Комплектация</label>
|
||
<select id="trimFilter">
|
||
<option value="all">Все</option>
|
||
<option value="Standard">Standard</option>
|
||
<option value="Comfort">Comfort</option>
|
||
<option value="Premium">Premium</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Цвет</label>
|
||
<select id="colorFilter">
|
||
<option value="all">Все цвета</option>
|
||
<option value="Белый">Белый</option>
|
||
<option value="Черный">Черный</option>
|
||
<option value="Серебристый">Серебристый</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Мин. цена</label>
|
||
<input type="number" id="minPrice" placeholder="От..." />
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Макс. цена</label>
|
||
<input type="number" id="maxPrice" placeholder="До..." />
|
||
</div>
|
||
<button class="filter-btn" onclick="applyFilters()">Применить</button>
|
||
<button class="filter-btn reset" onclick="resetFilters()">Сбросить</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="charts-grid">
|
||
<div class="chart-card">
|
||
<div class="chart-title">📈 Динамика продаж по дням</div>
|
||
<div class="chart-container">
|
||
<canvas id="salesTrendChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title">📍 Продажи по регионам (Топ-10)</div>
|
||
<div class="chart-container">
|
||
<canvas id="regionChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card full-width">
|
||
<div class="chart-title">🎯 Продажи по комплектациям и цветам</div>
|
||
<div class="chart-container" style="height: 350px;">
|
||
<canvas id="trimColorChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title">💰 Выручка по регионам (Топ-10)</div>
|
||
<div class="chart-container">
|
||
<canvas id="revenueChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title">🏆 Рейтинг регионов (Топ-10)</div>
|
||
<div class="chart-container">
|
||
<canvas id="ratingChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-section">
|
||
<div class="table-header">
|
||
<div class="table-title">📋 Детальные данные</div>
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<div class="search-box">
|
||
<input type="text" id="searchInput" placeholder="Поиск по таблице..." oninput="searchTable()" />
|
||
</div>
|
||
<button class="export-btn" onclick="exportToExcel()">📥 Экспорт в Excel</button>
|
||
</div>
|
||
</div>
|
||
<div style="overflow-x: auto;">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Регион</th>
|
||
<th>Комплектация</th>
|
||
<th>Цвет</th>
|
||
<th>Количество</th>
|
||
<th>Цена (₸)</th>
|
||
<th>Сумма (₸)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination" id="pagination"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let allData = [];
|
||
let filteredData = [];
|
||
let charts = {};
|
||
let currentPage = 1;
|
||
const rowsPerPage = 15;
|
||
|
||
function formatNumber(num) {
|
||
return new Intl.NumberFormat('ru-RU').format(num);
|
||
}
|
||
|
||
function formatCurrency(num) {
|
||
return new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'KZT', maximumFractionDigits: 0 }).format(num);
|
||
}
|
||
|
||
async function loadExcelData() {
|
||
try {
|
||
const response = await fetch('car_sales_kz_dashboard-1.xlsx');
|
||
const arrayBuffer = await response.arrayBuffer();
|
||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||
|
||
const rawSheet = workbook.Sheets['Сырые данные'];
|
||
const rawData = XLSX.utils.sheet_to_json(rawSheet, { header: 1 });
|
||
|
||
allData = rawData.slice(3).map(row => ({
|
||
date: row[0],
|
||
region: row[1],
|
||
trim: row[2],
|
||
color: row[3],
|
||
units: row[4],
|
||
price: row[5],
|
||
total: row[6]
|
||
})).filter(row => row.region && row.trim && row.units);
|
||
|
||
const overviewSheet = workbook.Sheets['Обзор'];
|
||
const overviewData = XLSX.utils.sheet_to_json(overviewSheet, { header: 1 });
|
||
|
||
const kpiRow = overviewData[4];
|
||
window.kpiData = {
|
||
totalUnits: kpiRow?.[1] || 0,
|
||
totalRevenue: kpiRow?.[4] || 0,
|
||
period: overviewData[3]?.[1] || ''
|
||
};
|
||
|
||
filteredData = [...allData];
|
||
initDashboard();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки данных:', error);
|
||
document.body.innerHTML = '<div class="loading">Ошибка загрузки данных. Проверьте файл Excel.</div>';
|
||
}
|
||
}
|
||
|
||
function initDashboard() {
|
||
populateFilters();
|
||
updateKPIs();
|
||
updateCharts();
|
||
updateTable();
|
||
}
|
||
|
||
function populateFilters() {
|
||
const regions = [...new Set(allData.map(d => d.region))].sort();
|
||
const regionSelect = document.getElementById('regionFilter');
|
||
regions.forEach(region => {
|
||
const option = document.createElement('option');
|
||
option.value = region;
|
||
option.textContent = region;
|
||
regionSelect.appendChild(option);
|
||
});
|
||
}
|
||
|
||
function updateKPIs() {
|
||
const totalUnits = filteredData.reduce((sum, d) => sum + d.units, 0);
|
||
const totalRevenue = filteredData.reduce((sum, d) => sum + d.total, 0);
|
||
const avgCheck = totalUnits > 0 ? totalRevenue / totalUnits : 0;
|
||
const regions = new Set(filteredData.map(d => d.region)).size;
|
||
|
||
document.getElementById('totalUnits').textContent = formatNumber(window.kpiData?.totalUnits || totalUnits);
|
||
document.getElementById('totalRevenue').textContent = formatCurrency(window.kpiData?.totalRevenue || totalRevenue).replace('KZT', '₸');
|
||
document.getElementById('avgCheck').textContent = formatCurrency(avgCheck).replace('KZT', '₸');
|
||
document.getElementById('totalRegions').textContent = regions;
|
||
|
||
if (window.kpiData?.period) {
|
||
document.getElementById('periodLabel').textContent = window.kpiData.period;
|
||
}
|
||
}
|
||
|
||
function updateCharts() {
|
||
updateSalesTrendChart();
|
||
updateRegionChart();
|
||
updateTrimColorChart();
|
||
updateRevenueChart();
|
||
updateRatingChart();
|
||
}
|
||
|
||
function updateSalesTrendChart() {
|
||
const daysData = {};
|
||
filteredData.forEach(d => {
|
||
if (d.date) {
|
||
daysData[d.date] = (daysData[d.date] || 0) + d.units;
|
||
}
|
||
});
|
||
|
||
const sortedDates = Object.keys(daysData).sort((a, b) => a - b);
|
||
const labels = sortedDates.map(date => {
|
||
const day = new Date((date - 25569) * 86400 * 1000);
|
||
return day.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' });
|
||
});
|
||
|
||
const ctx = document.getElementById('salesTrendChart').getContext('2d');
|
||
|
||
if (charts.trend) charts.trend.destroy();
|
||
|
||
charts.trend = new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [{
|
||
label: 'Продано авто',
|
||
data: sortedDates.map(date => daysData[date]),
|
||
borderColor: 'rgba(102, 126, 234, 1)',
|
||
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||
borderWidth: 3,
|
||
fill: true,
|
||
tension: 0.4,
|
||
pointRadius: 4,
|
||
pointBackgroundColor: 'rgba(102, 126, 234, 1)'
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||
},
|
||
x: {
|
||
grid: { display: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateRegionChart() {
|
||
const regionData = {};
|
||
filteredData.forEach(d => {
|
||
regionData[d.region] = (regionData[d.region] || 0) + d.units;
|
||
});
|
||
|
||
const sorted = Object.entries(regionData)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 10);
|
||
|
||
const ctx = document.getElementById('regionChart').getContext('2d');
|
||
|
||
if (charts.region) charts.region.destroy();
|
||
|
||
charts.region = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: sorted.map(([name]) => name),
|
||
datasets: [{
|
||
label: 'Количество авто',
|
||
data: sorted.map(([, value]) => value),
|
||
backgroundColor: 'rgba(102, 126, 234, 0.8)',
|
||
borderColor: 'rgba(102, 126, 234, 1)',
|
||
borderWidth: 1,
|
||
borderRadius: 8
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||
},
|
||
x: {
|
||
grid: { display: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateTrimColorChart() {
|
||
const trimData = {};
|
||
const colorData = {};
|
||
const trimColorData = {};
|
||
|
||
filteredData.forEach(d => {
|
||
trimData[d.trim] = (trimData[d.trim] || 0) + d.units;
|
||
colorData[d.color] = (colorData[d.color] || 0) + d.units;
|
||
const key = `${d.trim} - ${d.color}`;
|
||
trimColorData[key] = (trimColorData[key] || 0) + d.units;
|
||
});
|
||
|
||
const ctx = document.getElementById('trimColorChart').getContext('2d');
|
||
|
||
if (charts.trimColor) charts.trimColor.destroy();
|
||
|
||
const sortedData = Object.entries(trimColorData)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 9);
|
||
|
||
charts.trimColor = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: sortedData.map(([key]) => key),
|
||
datasets: [{
|
||
label: 'Продано авто',
|
||
data: sortedData.map(([, value]) => value),
|
||
backgroundColor: sortedData.map(([key]) => {
|
||
const trim = key.split(' - ')[0];
|
||
if (trim === 'Standard') return 'rgba(102, 126, 234, 0.8)';
|
||
if (trim === 'Comfort') return 'rgba(118, 75, 162, 0.8)';
|
||
return 'rgba(237, 137, 54, 0.8)';
|
||
}),
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(255, 255, 255, 1)',
|
||
borderRadius: 8
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||
},
|
||
x: {
|
||
grid: { display: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateRevenueChart() {
|
||
const revenueData = {};
|
||
filteredData.forEach(d => {
|
||
revenueData[d.region] = (revenueData[d.region] || 0) + (d.total || 0);
|
||
});
|
||
|
||
const sorted = Object.entries(revenueData)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 10);
|
||
|
||
const ctx = document.getElementById('revenueChart').getContext('2d');
|
||
|
||
if (charts.revenue) charts.revenue.destroy();
|
||
|
||
charts.revenue = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: sorted.map(([name]) => name),
|
||
datasets: [{
|
||
label: 'Выручка (₸)',
|
||
data: sorted.map(([, value]) => value),
|
||
backgroundColor: 'rgba(118, 75, 162, 0.8)',
|
||
borderColor: 'rgba(118, 75, 162, 1)',
|
||
borderWidth: 1,
|
||
borderRadius: 8
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||
ticks: {
|
||
callback: value => formatNumber(value / 1000000) + ' млн'
|
||
}
|
||
},
|
||
x: {
|
||
grid: { display: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateRatingChart() {
|
||
const regionData = {};
|
||
filteredData.forEach(d => {
|
||
regionData[d.region] = (regionData[d.region] || 0) + d.units;
|
||
});
|
||
|
||
const sorted = Object.entries(regionData)
|
||
.sort((a, b) => b[1] - a[1])
|
||
.slice(0, 10);
|
||
|
||
const total = Object.values(regionData).reduce((a, b) => a + b, 0);
|
||
|
||
const ctx = document.getElementById('ratingChart').getContext('2d');
|
||
|
||
if (charts.rating) charts.rating.destroy();
|
||
|
||
charts.rating = new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: sorted.map(([name]) => name),
|
||
datasets: [{
|
||
label: 'Доля рынка (%)',
|
||
data: sorted.map(([, value]) => ((value / total) * 100).toFixed(1)),
|
||
backgroundColor: sorted.map(([, , i], idx) => {
|
||
if (idx === 0) return 'rgba(237, 137, 54, 0.9)';
|
||
if (idx === 1) return 'rgba(118, 75, 162, 0.9)';
|
||
if (idx === 2) return 'rgba(102, 126, 234, 0.9)';
|
||
return 'rgba(203, 213, 224, 0.9)';
|
||
}),
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(255, 255, 255, 1)',
|
||
borderRadius: 8
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||
},
|
||
x: {
|
||
grid: { display: false }
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateTable() {
|
||
const startIndex = (currentPage - 1) * rowsPerPage;
|
||
const endIndex = startIndex + rowsPerPage;
|
||
const pageData = filteredData.slice(startIndex, endIndex);
|
||
|
||
const tbody = document.getElementById('tableBody');
|
||
tbody.innerHTML = pageData.map(row => `
|
||
<tr>
|
||
<td>${row.region}</td>
|
||
<td><span class="badge badge-${row.trim.toLowerCase()}">${row.trim}</span></td>
|
||
<td><span class="color-dot color-${row.color.toLowerCase()}"></span>${row.color}</td>
|
||
<td>${formatNumber(row.units)}</td>
|
||
<td>${formatCurrency(row.price).replace('KZT', '₸')}</td>
|
||
<td><strong>${formatCurrency(row.total).replace('KZT', '₸')}</strong></td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
updatePagination();
|
||
}
|
||
|
||
function updatePagination() {
|
||
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
|
||
const pagination = document.getElementById('pagination');
|
||
|
||
pagination.innerHTML = `
|
||
<button onclick="changePage(-1)" ${currentPage === 1 ? 'disabled' : ''}>← Назад</button>
|
||
<span>Страница ${currentPage} из ${totalPages || 1}</span>
|
||
<button onclick="changePage(1)" ${currentPage === totalPages ? 'disabled' : ''}>Вперёд →</button>
|
||
`;
|
||
}
|
||
|
||
function changePage(delta) {
|
||
currentPage += delta;
|
||
updateTable();
|
||
}
|
||
|
||
function applyFilters() {
|
||
const region = document.getElementById('regionFilter').value;
|
||
const trim = document.getElementById('trimFilter').value;
|
||
const color = document.getElementById('colorFilter').value;
|
||
const minPrice = parseFloat(document.getElementById('minPrice').value) || 0;
|
||
const maxPrice = parseFloat(document.getElementById('maxPrice').value) || Infinity;
|
||
|
||
filteredData = allData.filter(d => {
|
||
if (region !== 'all' && d.region !== region) return false;
|
||
if (trim !== 'all' && d.trim !== trim) return false;
|
||
if (color !== 'all' && d.color !== color) return false;
|
||
if (d.price < minPrice || d.price > maxPrice) return false;
|
||
return true;
|
||
});
|
||
|
||
currentPage = 1;
|
||
initDashboard();
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('regionFilter').value = 'all';
|
||
document.getElementById('trimFilter').value = 'all';
|
||
document.getElementById('colorFilter').value = 'all';
|
||
document.getElementById('minPrice').value = '';
|
||
document.getElementById('maxPrice').value = '';
|
||
filteredData = [...allData];
|
||
currentPage = 1;
|
||
initDashboard();
|
||
}
|
||
|
||
function searchTable() {
|
||
const query = document.getElementById('searchInput').value.toLowerCase();
|
||
filteredData = allData.filter(d =>
|
||
d.region.toLowerCase().includes(query) ||
|
||
d.trim.toLowerCase().includes(query) ||
|
||
d.color.toLowerCase().includes(query)
|
||
);
|
||
currentPage = 1;
|
||
updateTable();
|
||
updateKPIs();
|
||
}
|
||
|
||
function exportToExcel() {
|
||
const ws = XLSX.utils.json_to_sheet(filteredData);
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, 'Продажи');
|
||
XLSX.writeFile(wb, 'car_sales_export.xlsx');
|
||
}
|
||
|
||
loadExcelData();
|
||
</script>
|
||
</body>
|
||
</html>
|