ya-zagruzil-fayly-car-sales/index.html

964 lines
27 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.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>