335 lines
15 KiB
HTML
335 lines
15 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Дашборд: Анализ данных</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||
<style>
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #0f1218; color: #f4f4f5; }
|
||
.slide { display: none; min-height: 100vh; padding: 8vh 10vw; flex-direction: column; justify-content: center; }
|
||
.slide.on { display: flex; }
|
||
.slide h1 { font-size: clamp(34px, 6vw, 64px); margin-bottom: 24px; color: #00b8cc; }
|
||
.slide h2 { font-size: clamp(24px, 4vw, 42px); margin-bottom: 20px; color: #00b8cc; }
|
||
.slide li { font-size: clamp(18px, 2.6vw, 28px); line-height: 1.6; margin-bottom: 10px; list-style: none; padding-left: 28px; position: relative; }
|
||
.slide li::before { content: ""; position: absolute; left: 0; top: .55em; width: 12px; height: 12px; border-radius: 3px; background: #00b8cc; }
|
||
.num { position: fixed; bottom: 18px; right: 22px; color: #8a8a92; font-size: 14px; }
|
||
.nav { position: fixed; bottom: 14px; left: 22px; display: flex; gap: 8px; flex-wrap: wrap; max-width: 80vw; }
|
||
.nav button { border: 1px solid #333; background: #171719; color: #f4f4f5; border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; }
|
||
.nav .pptx { border-color: #00b8cc; color: #9beeff; }
|
||
.nav .refresh { border-color: #10b981; color: #6ee7b7; }
|
||
.metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-top: 30px; }
|
||
.metric-card { background: #1a1d24; border-radius: 12px; padding: 24px; border: 1px solid #2a2f3a; }
|
||
.metric-card h3 { font-size: 16px; color: #8a8a92; margin-bottom: 8px; }
|
||
.metric-card .value { font-size: clamp(28px, 4vw, 42px); font-weight: bold; color: #00b8cc; }
|
||
.metric-card .sub { font-size: 14px; color: #6a6a72; margin-top: 4px; }
|
||
table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: clamp(14px, 2vw, 18px); }
|
||
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #2a2f3a; }
|
||
th { color: #00b8cc; font-weight: 600; }
|
||
tr:hover { background: #1a1d24; }
|
||
.chart-placeholder { background: #1a1d24; border-radius: 12px; padding: 40px; text-align: center; margin: 20px 0; border: 1px solid #2a2f3a; }
|
||
.bar-row { display: flex; align-items: center; margin: 8px 0; }
|
||
.bar-label { width: 200px; font-size: 14px; color: #a1a1aa; }
|
||
.bar-container { flex: 1; background: #2a2f3a; border-radius: 4px; height: 24px; overflow: hidden; }
|
||
.bar { height: 100%; background: linear-gradient(90deg, #00b8cc, #0ea5e9); border-radius: 4px; transition: width 0.3s; }
|
||
.bar-value { margin-left: 12px; font-size: 14px; color: #00b8cc; min-width: 80px; text-align: right; }
|
||
.file-input { margin: 20px 0; }
|
||
.file-input input { display: none; }
|
||
.file-input label { display: inline-block; padding: 10px 20px; background: #00b8cc; color: #0f1218; border-radius: 8px; cursor: pointer; font-weight: 600; }
|
||
.file-input label:hover { background: #00d4ec; }
|
||
@media print {
|
||
.nav, .num, .file-input { display: none; }
|
||
.slide { display: flex; page-break-after: always; min-height: 100vh; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="deck"></div>
|
||
<div class="nav">
|
||
<button onclick="go(-1)">← Назад</button>
|
||
<button onclick="go(1)">Вперёд →</button>
|
||
<button class="pptx" onclick="downloadPptx()">📥 Скачать .pptx</button>
|
||
<button class="refresh" onclick="document.getElementById('fileInput').click()">📂 Загрузить новые данные</button>
|
||
</div>
|
||
<div class="num" id="num"></div>
|
||
|
||
<input type="file" id="fileInput" class="file-input" multiple accept=".xlsx,.xls" onchange="handleFiles(this.files)">
|
||
|
||
<script>
|
||
var DATA = {
|
||
installations: [],
|
||
rating: [],
|
||
presence: [],
|
||
presenceODT: []
|
||
};
|
||
|
||
var SLIDES = [];
|
||
|
||
function parseExcelFile(buffer, fileName) {
|
||
var wb = XLSX.read(buffer, { type: 'array' });
|
||
var sheet = wb.Sheets[wb.SheetNames[0]];
|
||
var json = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
||
return json;
|
||
}
|
||
|
||
function processData() {
|
||
if (DATA.installations.length === 0) return;
|
||
|
||
var installations = DATA.installations;
|
||
var rating = DATA.rating;
|
||
var presence = DATA.presence;
|
||
var presenceODT = DATA.presenceODT;
|
||
|
||
var totalPoints = 0;
|
||
var totalRevenue = 0;
|
||
var topPackages = [];
|
||
var regionalData = [];
|
||
var monthlyGrowth = [];
|
||
|
||
if (presence.length > 1) {
|
||
var lastRow = presence[presence.length - 1];
|
||
for (var i = 0; i < lastRow.length; i++) {
|
||
if (typeof lastRow[i] === 'number' && lastRow[i] > 100) {
|
||
totalPoints = lastRow[i];
|
||
break;
|
||
}
|
||
}
|
||
var revenueCols = presence[0].filter(function(h) { return h && h.toString().includes('Доход'); });
|
||
if (revenueCols.length > 0) {
|
||
var lastRevenueRow = presence[presence.length - 1];
|
||
var revenueStartIdx = presence[0].indexOf(revenueCols[revenueCols.length - 1]);
|
||
if (revenueStartIdx >= 0 && lastRevenueRow[revenueStartIdx]) {
|
||
totalRevenue = lastRevenueRow[revenueStartIdx];
|
||
}
|
||
}
|
||
}
|
||
|
||
if (rating.length > 1) {
|
||
for (var i = 1; i < Math.min(rating.length, 11); i++) {
|
||
var row = rating[i];
|
||
if (row[0] && row[1] && row[2]) {
|
||
topPackages.push({
|
||
name: row[0],
|
||
points: row[2] || 0,
|
||
revenue: row[3] || 0
|
||
});
|
||
}
|
||
}
|
||
topPackages.sort(function(a, b) { return b.revenue - a.revenue; });
|
||
}
|
||
|
||
if (presenceODT.length > 1) {
|
||
var odtRows = presenceODT.filter(function(row) {
|
||
return row[0] && row[0].toString().includes('ОДТ') && !row[0].toString().includes('Итоги');
|
||
});
|
||
odtRows.slice(0, 8).forEach(function(row) {
|
||
var odtName = row[1] || row[0];
|
||
var totalVal = row[2] || row[row.length - 1] || 0;
|
||
regionalData.push({ name: odtName, value: totalVal });
|
||
});
|
||
regionalData.sort(function(a, b) { return b.value - a.value; });
|
||
}
|
||
|
||
if (installations.length > 1) {
|
||
var months = ['202601', '202602', '202603', '202604', '202605', '202606', '202607', '202608'];
|
||
var totalsRow = installations.find(function(row) { return row[0] === 'Итоги' || row[1] === 'Итоги'; });
|
||
if (totalsRow) {
|
||
months.forEach(function(m, idx) {
|
||
var colIdx = installations[0].indexOf(parseFloat(m));
|
||
if (colIdx < 0) colIdx = installations[0].findIndex(function(c) { return c && c.toString() === m; });
|
||
if (colIdx >= 0 && totalsRow[colIdx]) {
|
||
monthlyGrowth.push({ month: m.slice(4), value: totalsRow[colIdx] });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
var revenueFormatted = totalRevenue >= 1000 ? (totalRevenue / 1000).toFixed(1) + ' млн' : totalRevenue.toFixed(0);
|
||
|
||
SLIDES = [
|
||
{
|
||
title: "Дашборд: Анализ данных",
|
||
bullets: [
|
||
"Период: Январь - Август 2026",
|
||
"Всего точек: " + totalPoints.toLocaleString('ru-RU'),
|
||
"Доход: " + revenueFormatted + " тыс.₸",
|
||
"Пакетов в работе: " + (rating.length - 1),
|
||
"",
|
||
"Навигация: стрелки ← → или кнопки внизу",
|
||
"Загрузите новые файлы для обновления"
|
||
]
|
||
},
|
||
{
|
||
title: "Ключевые метрики",
|
||
bullets: [],
|
||
custom: function() {
|
||
var cards = [
|
||
{ label: "Всего точек", value: totalPoints.toLocaleString('ru-RU'), sub: "Активные подключения" },
|
||
{ label: "Доход", value: revenueFormatted, sub: "тыс. ₸ в месяц" },
|
||
{ label: "Пакетов", value: (rating.length - 1).toString(), sub: "Тарифных планов" },
|
||
{ label: "Регионов", value: regionalData.length.toString(), sub: "ОДТ в отчёте" }
|
||
];
|
||
return '<div class="metric-grid">' + cards.map(function(c) {
|
||
return '<div class="metric-card"><h3>' + c.label + '</h3><div class="value">' + c.value + '</div><div class="sub">' + c.sub + '</div></div>';
|
||
}).join('') + '</div>';
|
||
}
|
||
},
|
||
{
|
||
title: "Топ пакетов по доходу",
|
||
bullets: [],
|
||
custom: function() {
|
||
if (topPackages.length === 0) return '<div class="chart-placeholder">Нет данных</div>';
|
||
var maxRev = Math.max.apply(null, topPackages.map(function(p) { return p.revenue; }));
|
||
return topPackages.slice(0, 8).map(function(p) {
|
||
var pct = maxRev > 0 ? (p.revenue / maxRev * 100) : 0;
|
||
var revStr = p.revenue >= 1000 ? (p.revenue/1000).toFixed(1) + ' млн' : p.revenue.toFixed(0);
|
||
return '<div class="bar-row"><div class="bar-label">' + p.name + '</div><div class="bar-container"><div class="bar" style="width:' + pct + '%"></div></div><div class="bar-value">' + revStr + ' тыс.₸</div></div>';
|
||
}).join('');
|
||
}
|
||
},
|
||
{
|
||
title: "Топ пакетов по точкам",
|
||
bullets: [],
|
||
custom: function() {
|
||
if (topPackages.length === 0) return '<div class="chart-placeholder">Нет данных</div>';
|
||
var maxPts = Math.max.apply(null, topPackages.map(function(p) { return p.points; }));
|
||
return topPackages.slice(0, 8).map(function(p) {
|
||
var pct = maxPts > 0 ? (p.points / maxPts * 100) : 0;
|
||
return '<div class="bar-row"><div class="bar-label">' + p.name + '</div><div class="bar-container"><div class="bar" style="width:' + pct + '%"></div></div><div class="bar-value">' + p.points.toLocaleString('ru-RU') + '</div></div>';
|
||
}).join('');
|
||
}
|
||
},
|
||
{
|
||
title: "Региональное распределение",
|
||
bullets: [],
|
||
custom: function() {
|
||
if (regionalData.length === 0) return '<div class="chart-placeholder">Нет данных</div>';
|
||
var maxVal = Math.max.apply(null, regionalData.map(function(r) { return r.value; }));
|
||
return regionalData.slice(0, 7).map(function(r) {
|
||
var pct = maxVal > 0 ? (r.value / maxVal * 100) : 0;
|
||
return '<div class="bar-row"><div class="bar-label">' + r.name + '</div><div class="bar-container"><div class="bar" style="width:' + pct + '%"></div></div><div class="bar-value">' + r.value.toLocaleString('ru-RU') + '</div></div>';
|
||
}).join('');
|
||
}
|
||
},
|
||
{
|
||
title: "Динамика подключений",
|
||
bullets: [],
|
||
custom: function() {
|
||
if (monthlyGrowth.length === 0) return '<div class="chart-placeholder">Нет данных</div>';
|
||
var maxVal = Math.max.apply(null, monthlyGrowth.map(function(m) { return m.value; }));
|
||
return monthlyGrowth.map(function(m) {
|
||
var pct = maxVal > 0 ? (m.value / maxVal * 100) : 0;
|
||
var monthName = ['01','02','03','04','05','06','07','08'][parseInt(m.month)-1] || m.month;
|
||
return '<div class="bar-row"><div class="bar-label">Месяц ' + monthName + '</div><div class="bar-container"><div class="bar" style="width:' + pct + '%"></div></div><div class="bar-value">' + m.value.toLocaleString('ru-RU') + '</div></div>';
|
||
}).join('');
|
||
}
|
||
},
|
||
{
|
||
title: "Итоги",
|
||
bullets: [
|
||
"Дашборд готов к использованию",
|
||
"Загрузите новые Excel-файлы через кнопку «Загрузить новые данные»",
|
||
"Данные обновятся автоматически",
|
||
"Скачайте презентацию в формате .pptx",
|
||
"Все метрики пересчитаются для новых данных"
|
||
]
|
||
}
|
||
];
|
||
}
|
||
|
||
var cur = 0;
|
||
|
||
function render() {
|
||
var deck = document.getElementById("deck");
|
||
deck.innerHTML = SLIDES.map(function (s, i) {
|
||
var content = s.custom ? s.custom() : '<ul>' + s.bullets.map(function (b) { return "<li>" + b + "</li>"; }).join("") + '</ul>';
|
||
return '<section class="slide' + (i === cur ? " on" : "") + '">' +
|
||
(s.title.includes('Дашборд') ? '<h1>' + s.title + '</h1>' : '<h2>' + s.title + '</h2>') +
|
||
content + '</section>';
|
||
}).join("");
|
||
document.getElementById("num").textContent = (cur + 1) + " / " + SLIDES.length;
|
||
}
|
||
|
||
function go(d) {
|
||
cur = Math.min(SLIDES.length - 1, Math.max(0, cur + d));
|
||
render();
|
||
}
|
||
|
||
document.addEventListener("keydown", function (e) {
|
||
if (e.key === "ArrowRight" || e.key === " ") go(1);
|
||
if (e.key === "ArrowLeft") go(-1);
|
||
});
|
||
|
||
function downloadPptx() {
|
||
var p = new PptxGenJS();
|
||
p.defineLayout({ name: "W", width: 13.33, height: 7.5 });
|
||
p.layout = "W";
|
||
|
||
SLIDES.forEach(function (s) {
|
||
var sl = p.addSlide();
|
||
sl.background = { color: "0F1218" };
|
||
|
||
var titleStyle = { x: 0.7, y: 0.5, w: 12, h: 1.2, fontSize: 32, bold: true, color: "00B8CC" };
|
||
if (s.title.includes('Дашборд')) {
|
||
titleStyle.fontSize = 36;
|
||
}
|
||
sl.addText(s.title, titleStyle);
|
||
|
||
if (s.customText) {
|
||
sl.addText(s.customText, { x: 0.9, y: 2.0, w: 11.5, h: 4.8, fontSize: 18, color: "E4E4E7" });
|
||
} else if (s.bullets && s.bullets.length > 0) {
|
||
var bulletPoints = s.bullets.filter(function(b) { return b; }).map(function(b) {
|
||
return { text: b, options: { bullet: true, breakLine: true, fontSize: 20, color: "E4E4E7" } };
|
||
});
|
||
sl.addText(bulletPoints, { x: 0.9, y: 2.0, w: 11.5, h: 4.8, lineSpacingMultiple: 1.4 });
|
||
}
|
||
});
|
||
|
||
p.writeFile({ fileName: "dashboard_" + new Date().toISOString().slice(0,10) + ".pptx" });
|
||
}
|
||
|
||
function handleFiles(files) {
|
||
var loaded = 0;
|
||
var expected = 4;
|
||
|
||
for (var i = 0; i < files.length; i++) {
|
||
var file = files[i];
|
||
var reader = new FileReader();
|
||
|
||
reader.onload = function(e) {
|
||
var buffer = e.target.result;
|
||
var fileName = e.target.fileName.toLowerCase();
|
||
var data = parseExcelFile(buffer, fileName);
|
||
|
||
if (fileName.includes('установки') || fileName.includes('переключения')) {
|
||
DATA.installations = data;
|
||
} else if (fileName.includes('рейтинг')) {
|
||
DATA.rating = data;
|
||
} else if (fileName.includes('наличие') && fileName.includes('одт')) {
|
||
DATA.presenceODT = data;
|
||
} else if (fileName.includes('наличие') && !fileName.includes('одт')) {
|
||
DATA.presence = data;
|
||
}
|
||
|
||
loaded++;
|
||
if (loaded >= expected || loaded >= files.length) {
|
||
processData();
|
||
cur = 0;
|
||
render();
|
||
alert('Данные обновлены! Слайдов: ' + SLIDES.length);
|
||
}
|
||
};
|
||
|
||
reader.fileName = file.name;
|
||
reader.readAsArrayBuffer(file);
|
||
}
|
||
}
|
||
|
||
processData();
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|