409 lines
21 KiB
HTML
409 lines
21 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Безналичные поступления — динамика за 3 года</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/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||
<style>
|
||
.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; }
|
||
.kt-ai-app-top { position: sticky; top: 0; z-index: 5; }
|
||
.kt-ai-app-main { flex: 1; padding: var(--kt-ai-space-7) 0 var(--kt-ai-space-9); }
|
||
.kt-ai-drop {
|
||
border: 2px dashed var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl);
|
||
padding: var(--kt-ai-space-8) var(--kt-ai-space-7); text-align: center;
|
||
cursor: pointer; transition: border-color .15s, background .15s;
|
||
}
|
||
.kt-ai-drop:hover, .kt-ai-drop.over { border-color: var(--kt-ai-primary); background: var(--kt-ai-bg-soft); }
|
||
.kt-ai-drop-ic { display: inline-flex; margin-bottom: var(--kt-ai-space-3); color: var(--kt-ai-primary); }
|
||
.kt-ai-drop-ic svg { width: 34px; height: 34px; }
|
||
.kt-ai-drop-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); display: block; margin-bottom: 4px; }
|
||
.kt-ai-drop-hint { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); display: block; line-height: 1.5; }
|
||
.kt-ai-files { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-2); margin-top: var(--kt-ai-space-4); }
|
||
.kt-ai-filechip {
|
||
display: inline-flex; align-items: center; gap: var(--kt-ai-space-2);
|
||
background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border);
|
||
border-radius: var(--kt-ai-radius-full); padding: 5px 8px 5px 12px;
|
||
font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium);
|
||
}
|
||
.kt-ai-filechip button {
|
||
border: 0; background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer;
|
||
display: inline-flex; padding: 2px; border-radius: 50%; line-height: 0;
|
||
}
|
||
.kt-ai-filechip button:hover { color: var(--kt-ai-danger); }
|
||
.kt-ai-filechip svg { width: 14px; height: 14px; }
|
||
.kt-ai-stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
|
||
.kt-ai-stat { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||
.kt-ai-stat-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); display: block; margin-bottom: var(--kt-ai-space-2); }
|
||
.kt-ai-stat-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); }
|
||
.kt-ai-stat-delta { display: block; margin-top: var(--kt-ai-space-1); font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); }
|
||
.delta-up { color: var(--kt-ai-primary); }
|
||
.delta-down { color: var(--kt-ai-danger); }
|
||
.kt-ai-charts { display: grid; grid-template-columns: 3fr 2fr; gap: var(--kt-ai-space-5); margin-bottom: var(--kt-ai-space-6); }
|
||
@media (max-width: 900px) { .kt-ai-charts { grid-template-columns: 1fr; } }
|
||
.kt-ai-chart-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-6); }
|
||
.kt-ai-chart-card h3 { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin: 0 0 var(--kt-ai-space-5); }
|
||
.kt-ai-chart svg { width: 100%; height: auto; display: block; }
|
||
.kt-ai-legend { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); }
|
||
.kt-ai-legend span { display: inline-flex; align-items: center; gap: 6px; color: var(--kt-ai-fg-muted); }
|
||
.kt-ai-legend i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
|
||
.kt-ai-tbl-wrap { overflow-x: auto; border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); background: var(--kt-ai-card-bg); }
|
||
table.kt-ai-table { width: 100%; border-collapse: collapse; font-size: var(--kt-ai-text-sm); }
|
||
.kt-ai-table th, .kt-ai-table td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--kt-ai-divider); white-space: nowrap; }
|
||
.kt-ai-table thead th { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); font-weight: var(--kt-ai-weight-medium); }
|
||
.kt-ai-table tbody tr:last-child td { border-bottom: 0; }
|
||
.kt-ai-table td.num, .kt-ai-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.kt-ai-empty { padding: var(--kt-ai-space-10) var(--kt-ai-space-7); text-align: center; color: var(--kt-ai-fg-muted); }
|
||
.kt-ai-empty svg { width: 40px; height: 40px; margin-bottom: var(--kt-ai-space-3); opacity: .5; }
|
||
.kt-ai-actions { margin-top: var(--kt-ai-space-4); display: flex; align-items: center; gap: var(--kt-ai-space-3); flex-wrap: wrap; }
|
||
@media print {
|
||
.kt-ai-app-top, .kt-ai-drop, .kt-ai-actions { display: none; }
|
||
.kt-ai-app-main { padding: 0; }
|
||
.kt-ai-charts { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="kt-ai-app">
|
||
<div class="kt-ai-app-top">
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<strong>Безналичные поступления</strong>
|
||
<span>Динамика за 3 года · сравнение</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<label class="kt-ai-btn" data-variant="primary" style="cursor:pointer">
|
||
<input type="file" id="fileInput" accept=".xlsx,.xls" multiple style="display:none" onchange="handleFiles(this.files)">
|
||
Загрузить Excel
|
||
</label>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="resetData()">Сбросить</button>
|
||
</div>
|
||
</header>
|
||
</div>
|
||
|
||
<main class="kt-ai-app-main">
|
||
<div class="kt-ai-wrap">
|
||
|
||
<div class="kt-ai-drop" id="drop" onclick="document.getElementById('fileInput').click()">
|
||
<span class="kt-ai-drop-ic">
|
||
<svg class="kt-icon" aria-hidden="true"><use href="#upload"></use></svg>
|
||
</span>
|
||
<span class="kt-ai-drop-title">Перетащите файлы .xlsx сюда или нажмите, чтобы выбрать</span>
|
||
<span class="kt-ai-drop-hint" id="dropHint">
|
||
Формат: столбцы «Год», «Наименование контрагента», «Сумма» (в любой последовательности, с 2023 по 2025).
|
||
</span>
|
||
<div class="kt-ai-files" id="fileList"></div>
|
||
</div>
|
||
|
||
<div class="kt-ai-actions" id="demoBar" style="margin-top:var(--kt-ai-space-4)">
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="loadDemo()">Показать пример данных</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" style="display:none" id="demoClear" onclick="clearDemo()">Очистить пример</button>
|
||
<span style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">или загрузите свои файлы .xlsx</span>
|
||
</div>
|
||
|
||
<div id="results"></div>
|
||
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
var YEAR_COLORS = ["8B95A5", "3E9BBF", "00B8CC"];
|
||
var COUNTERPARTY_COLORS = ["00B8CC", "F4B942", "7C6FE8", "4CC38A", "F472A0", "5B8DEF", "D96C4F", "8A8A92"];
|
||
var files = [];
|
||
var rows = [];
|
||
var demoOn = false;
|
||
|
||
var DEMO = [
|
||
{ year: 2023, name: "KT Bank", amount: 12500000 },
|
||
{ year: 2023, name: "Halyk Pay", amount: 8400000 },
|
||
{ year: 2023, name: "Kaspi Online", amount: 15200000 },
|
||
{ year: 2023, name: "Beeline Payments", amount: 3600000 },
|
||
{ year: 2023, name: "Qysqali", amount: 2100000 },
|
||
{ year: 2024, name: "KT Bank", amount: 14800000 },
|
||
{ year: 2024, name: "Halyk Pay", amount: 9900000 },
|
||
{ year: 2024, name: "Kaspi Online", amount: 18700000 },
|
||
{ year: 2024, name: "Beeline Payments", amount: 4100000 },
|
||
{ year: 2024, name: "Qysqali", amount: 2450000 },
|
||
{ year: 2025, name: "KT Bank", amount: 17300000 },
|
||
{ year: 2025, name: "Halyk Pay", amount: 11200000 },
|
||
{ year: 2025, name: "Kaspi Online", amount: 22400000 },
|
||
{ year: 2025, name: "Beeline Payments", amount: 3800000 },
|
||
{ year: 2025, name: "Qysqali", amount: 2900000 },
|
||
{ year: 2025, name: "Paytech Almaty", amount: 1500000 }
|
||
];
|
||
|
||
function loadDemo() {
|
||
if (demoOn) return;
|
||
demoOn = true;
|
||
rows = rows.concat(DEMO);
|
||
render();
|
||
}
|
||
|
||
function clearDemo() {
|
||
demoOn = false;
|
||
rows = rows.filter(function (r) {
|
||
return !DEMO.some(function (d) { return d.year === r.year && d.name === r.name && d.amount === r.amount; });
|
||
});
|
||
render();
|
||
}
|
||
|
||
var fmt = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 });
|
||
|
||
function money(v) { return fmt.format(Math.round(v)); }
|
||
function kfmt(v) {
|
||
var a = Math.abs(v);
|
||
if (a >= 1e9) return (v / 1e9).toFixed(1).replace(".0","") + " млрд";
|
||
if (a >= 1e6) return (v / 1e6).toFixed(1).replace(".0","") + " млн";
|
||
if (a >= 1e3) return (v / 1e3).toFixed(0) + " тыс";
|
||
return String(Math.round(v));
|
||
}
|
||
|
||
function parseNumber(v) {
|
||
if (v == null) return 0;
|
||
if (typeof v === "number") return v;
|
||
var s = String(v).replace(/\s/g, "").replace(/\.(?=\d{3}(\D|$))/g, "").replace(",", ".");
|
||
var n = parseFloat(s);
|
||
return isNaN(n) ? 0 : n;
|
||
}
|
||
|
||
function detectSheetToRows(sheet, wb) {
|
||
var a = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null });
|
||
if (!a.length) return null;
|
||
var hdr = a[0].map(function (h) { return String(h == null ? "" : h).trim(); });
|
||
var idx = { year: -1, name: -1, amount: -1 };
|
||
hdr.forEach(function (h, i) {
|
||
var l = h.toLowerCase();
|
||
if (idx.year < 0 && /год|year/.test(l)) idx.year = i;
|
||
if (idx.name < 0 && /контрагент|partner|наименование|назван|клиент|компан/.test(l)) idx.name = i;
|
||
if (idx.amount < 0 && /(сумм|amount|поступл|платеж)/.test(l)) idx.amount = i;
|
||
});
|
||
if (idx.year < 0 || idx.name < 0 || idx.amount < 0) return null;
|
||
var out = [];
|
||
for (var i = 1; i < a.length; i++) {
|
||
var r = a[i];
|
||
if (!r || r.every(function (c) { return c == null || String(c).trim() === ""; })) continue;
|
||
var y = parseInt(r[idx.year], 10);
|
||
var n = r[idx.name];
|
||
var amt = parseNumber(r[idx.amount]);
|
||
if (!y || !n || !amt) continue;
|
||
out.push({ year: y, name: String(n).trim(), amount: amt });
|
||
}
|
||
return out.length ? out : null;
|
||
}
|
||
|
||
function handleFiles(list) {
|
||
Array.prototype.forEach.call(list, function (f) {
|
||
if (!/\.(xlsx|xls)$/i.test(f.name)) return;
|
||
files.push({ name: f.name, file: f });
|
||
f.arrayBuffer().then(function (buf) {
|
||
var wb = XLSX.read(buf, { type: "array" });
|
||
for (var si = 0; si < wb.SheetNames.length; si++) {
|
||
var got = detectSheetToRows(wb.Sheets[wb.SheetNames[si]], wb);
|
||
if (got) { rows = rows.concat(got); break; }
|
||
}
|
||
render();
|
||
});
|
||
});
|
||
document.getElementById("fileInput").value = "";
|
||
renderFileList();
|
||
}
|
||
|
||
function removeFile(i) {
|
||
files.splice(i, 1);
|
||
rows = rows.filter(function (r) { return true; });
|
||
// пересобрать сложно, поэтому просто перечитаем оставшиеся файлы
|
||
rebuildRows();
|
||
}
|
||
|
||
function rebuildRows() {
|
||
rows = [];
|
||
var pending = 0;
|
||
files.forEach(function (f) {
|
||
pending++;
|
||
f.file.arrayBuffer().then(function (buf) {
|
||
var wb = XLSX.read(buf, { type: "array" });
|
||
for (var si = 0; si < wb.SheetNames.length; si++) {
|
||
var got = detectSheetToRows(wb.Sheets[wb.SheetNames[si]], wb);
|
||
if (got) { rows = rows.concat(got); break; }
|
||
}
|
||
pending--;
|
||
if (pending === 0) { renderFileList(); render(); }
|
||
});
|
||
});
|
||
if (pending === 0) { renderFileList(); render(); }
|
||
}
|
||
|
||
function resetData() {
|
||
files = []; rows = []; demoOn = false;
|
||
renderFileList(); render();
|
||
}
|
||
|
||
function renderFileList() {
|
||
var el = document.getElementById("fileList");
|
||
el.innerHTML = files.map(function (f, i) {
|
||
return '<span class="kt-ai-filechip">' + f.name +
|
||
' <button onclick="removeFile(' + i + ')" title="Убрать файл"><svg class="kt-icon" aria-hidden="true"><use href="#x"></use></svg></button></span>';
|
||
}).join("");
|
||
}
|
||
|
||
function aggregate() {
|
||
var byYear = {};
|
||
var byPair = {};
|
||
var byYearName = {};
|
||
rows.forEach(function (r) {
|
||
byYear[r.year] = (byYear[r.year] || 0) + r.amount;
|
||
var key = r.year + "|" + r.name;
|
||
byPair[key] = (byPair[key] || 0) + r.amount;
|
||
if (!byYearName[r.year]) byYearName[r.year] = {};
|
||
byYearName[r.year][r.name] = (byYearName[r.year][r.name] || 0) + r.amount;
|
||
});
|
||
var years = Object.keys(byYear).map(Number).sort();
|
||
return { byYear: byYear, byPair: byPair, byYearName: byYearName, years: years };
|
||
}
|
||
|
||
function chartGrouped(data) {
|
||
var years = data.years;
|
||
var names = [];
|
||
data.byYearName.forEach(function (m) {
|
||
Object.keys(m).forEach(function (n) { if (names.indexOf(n) < 0) names.push(n); });
|
||
});
|
||
var max = 0;
|
||
names.forEach(function (n) {
|
||
years.forEach(function (y) {
|
||
var v = (data.byYearName[y] && data.byYearName[y][n]) || 0;
|
||
if (v > max) max = v;
|
||
});
|
||
});
|
||
var W = 760, H = 380, padL = 60, padB = 80, padT = 20, padR = 20;
|
||
var plotW = W - padL - padR, plotH = H - padT - padB;
|
||
var band = plotW / Math.max(years.length, 1);
|
||
var bw = Math.min(60, (band - 10) / Math.max(names.length, 1));
|
||
var svg = ['<svg viewBox="0 0 ' + W + ' ' + H + '" class="kt-ai-chart" xmlns="http://www.w3.org/2000/svg">'];
|
||
for (var g = 0; g <= 4; g++) {
|
||
var gv = max * g / 4;
|
||
var gy = padT + plotH - plotH * g / 4;
|
||
svg.push('<line x1="' + padL + '" y1="' + gy + '" x2="' + (W - padR) + '" y2="' + gy + '" stroke="var(--kt-ai-divider)"/>');
|
||
svg.push('<text x="' + (padL - 8) + '" y="' + (gy + 4) + '" text-anchor="end" font-size="11" fill="var(--kt-ai-fg-muted)">' + kfmt(gv) + '</text>');
|
||
}
|
||
years.forEach(function (y, yi) {
|
||
var cx = padL + band * yi + band / 2;
|
||
names.forEach(function (n, ni) {
|
||
var v = (data.byYearName[y] && data.byYearName[y][n]) || 0;
|
||
var h = max ? (v / max) * plotH : 0;
|
||
var bx = cx - (names.length * bw + (names.length - 1) * 3) / 2 + ni * (bw + 3);
|
||
var by = padT + plotH - h;
|
||
svg.push('<rect x="' + bx.toFixed(1) + '" y="' + by.toFixed(1) + '" width="' + bw.toFixed(1) + '" height="' + h.toFixed(1) + '" rx="4" fill="' + COUNTERPARTY_COLORS[ni % COUNTERPARTY_COLORS.length] + '"/>');
|
||
});
|
||
svg.push('<text x="' + cx + '" y="' + (H - padB + 22) + '" text-anchor="middle" font-size="13" font-weight="600" fill="var(--kt-ai-fg)">' + y + '</text>');
|
||
});
|
||
svg.push('</svg>');
|
||
return svg;
|
||
}
|
||
|
||
function chartLine(data) {
|
||
var years = data.years;
|
||
var vals = years.map(function (y) { return data.byYear[y]; });
|
||
var max = Math.max.apply(null, vals.concat([1]));
|
||
var W = 520, H = 300, padL = 60, padB = 40, padT = 24, padR = 24;
|
||
var plotW = W - padL - padR, plotH = H - padT - padB;
|
||
var pt = function (i) {
|
||
var x = years.length > 1 ? padL + plotW * i / (years.length - 1) : padL + plotW / 2;
|
||
var y = padT + plotH - (vals[i] / max) * plotH;
|
||
return { x: x, y: y };
|
||
};
|
||
var n = years.length;
|
||
var svg = ['<svg viewBox="0 0 ' + W + ' ' + H + '" class="kt-ai-chart" xmlns="http://www.w3.org/2000/svg">'];
|
||
for (var g = 0; g <= 4; g++) {
|
||
var gv = max * g / 4;
|
||
var gy = padT + plotH - plotH * g / 4;
|
||
svg.push('<line x1="' + padL + '" y1="' + gy + '" x2="' + (W - padR) + '" y2="' + gy + '" stroke="var(--kt-ai-divider)"/>');
|
||
svg.push('<text x="' + (padL - 8) + '" y="' + (gy + 4) + '" text-anchor="end" font-size="11" fill="var(--kt-ai-fg-muted)">' + kfmt(gv) + '</text>');
|
||
}
|
||
if (n > 1) {
|
||
var area = vals.map(function (v, i) { var p = pt(i); return (i ? "L" : "M") + p.x.toFixed(1) + " " + p.y.toFixed(1); }).join(" ");
|
||
area += " L" + pt(n - 1).x.toFixed(1) + " " + (padT + plotH) + " L" + pt(0).x.toFixed(1) + " " + (padT + plotH) + " Z";
|
||
svg.push('<path d="' + area + '" fill="var(--kt-ai-primary)" opacity="0.12"/>');
|
||
svg.push('<path d="' + vals.map(function (v, i) { var p = pt(i); return (i ? "L" : "M") + p.x.toFixed(1) + " " + p.y.toFixed(1); }).join(" ") + '" fill="none" stroke="var(--kt-ai-primary)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>');
|
||
}
|
||
years.forEach(function (y, i) {
|
||
var p = pt(i);
|
||
svg.push('<circle cx="' + p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="5" fill="var(--kt-ai-primary)" stroke="var(--kt-ai-bg)" stroke-width="2"/>');
|
||
svg.push('<text x="' + p.x.toFixed(1) + '" y="' + (p.y - 12) + '" text-anchor="middle" font-size="11" font-weight="600" fill="var(--kt-ai-fg)">' + kfmt(vals[i]) + '</text>');
|
||
svg.push('<text x="' + p.x.toFixed(1) + '" y="' + (H - padB + 22) + '" text-anchor="middle" font-size="12" fill="var(--kt-ai-fg-muted)">' + y + '</text>');
|
||
});
|
||
svg.push('</svg>');
|
||
return svg;
|
||
}
|
||
|
||
function render() {
|
||
renderFileList();
|
||
var el = document.getElementById("results");
|
||
if (!rows.length) {
|
||
el.innerHTML = '<div class="kt-ai-empty"><svg class="kt-icon" aria-hidden="true"><use href="#chart"></use></svg><div>Загрузите файлы с данными — здесь появятся статистика, графики и таблица контрагентов.</div></div>';
|
||
return;
|
||
}
|
||
var d = aggregate();
|
||
var years = d.years;
|
||
var total = 0;
|
||
years.forEach(function (y) { total += d.byYear[y]; });
|
||
var firstY = years[0], lastY = years[years.length - 1];
|
||
var delta = d.byYear[firstY] ? ((d.byYear[lastY] - d.byYear[firstY]) / d.byYear[firstY] * 100) : null;
|
||
var names = [];
|
||
d.byYearName.forEach(function (m) { Object.keys(m).forEach(function (n) { if (names.indexOf(n) < 0) names.push(n); }); });
|
||
|
||
var stats = years.map(function (y) {
|
||
var prev = years.indexOf(y) > 0 ? d.byYear[years[years.indexOf(y) - 1]] : null;
|
||
var dl = prev ? ((d.byYear[y] - prev) / prev * 100) : null;
|
||
var dlHtml = dl == null ? '<span class="kt-ai-stat-delta">—</span>'
|
||
: '<span class="kt-ai-stat-delta ' + (dl >= 0 ? "delta-up" : "delta-down") + '">' + (dl >= 0 ? "↑ " : "↓ ") + Math.abs(dl).toFixed(1) + "% к пред. году</span>";
|
||
return '<div class="kt-ai-stat"><span class="kt-ai-stat-label">Поступления, ' + y + '</span><span class="kt-ai-stat-value">' + money(d.byYear[y]) + '</span>' + dlHtml + '</div>';
|
||
}).join("");
|
||
|
||
var tableRows = names.map(function (n, i) {
|
||
var cells = years.map(function (y) {
|
||
return '<td class="num">' + ((d.byYearName[y] && d.byYearName[y][n]) ? money(d.byYearName[y][n]) : "—") + '</td>';
|
||
}).join("");
|
||
var sum = 0, present = false;
|
||
years.forEach(function (y) { if (d.byYearName[y] && d.byYearName[y][n]) { sum += d.byYearName[y][n]; present = true; } });
|
||
var share = present && total ? (sum / total * 100).toFixed(1) + " %" : "—";
|
||
return '<tr><td>' + n + '</td>' + cells + '<td class="num"><strong>' + (present ? money(sum) : "—") + '</strong></td><td class="num">' + share + '</td></tr>';
|
||
}).join("");
|
||
var totalRow = '<tr><td><strong>Итого</strong></td>' + years.map(function (y) { return '<td class="num"><strong>' + money(d.byYear[y]) + '</strong></td>'; }).join("") + '<td class="num"><strong>' + money(total) + '</strong></td><td class="num">100%</td></tr>';
|
||
|
||
el.innerHTML =
|
||
'<div class="kt-ai-stat-grid">' + stats +
|
||
(delta != null ? '<div class="kt-ai-stat"><span class="kt-ai-stat-label">Смена за ' + firstY + ' → ' + lastY + '</span><span class="kt-ai-stat-value ' + (delta >= 0 ? "delta-up" : "delta-down") + '">' + (delta >= 0 ? "+" : "") + delta.toFixed(1) + '%</span></div>' : '') +
|
||
'</div>' +
|
||
'<div class="kt-ai-charts">' +
|
||
'<div class="kt-ai-chart-card"><h3>Поступления по годам</h3>' + chartLine(d) + '</div>' +
|
||
'<div class="kt-ai-chart-card"><h3>По контрагентам</h3>' + chartGrouped(d) +
|
||
'<div class="kt-ai-legend">' + names.map(function (n, i) { return '<span><i style="background:' + COUNTERPARTY_COLORS[i % COUNTERPARTY_COLORS.length] + '"></i>' + n + '</span>'; }).join("") + '</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="kt-ai-chart-card" style="margin-bottom:var(--kt-ai-space-4)"><h3 style="margin-bottom:var(--kt-ai-space-4)">Контрагенты и суммы, ' + years.join(" / ") + '</h3>' +
|
||
'<div class="kt-ai-tbl-wrap"><table class="kt-ai-table"><thead><tr><th>Контрагент</th>' + years.map(function (y) { return '<th class="num">' + y + '</th>'; }).join("") + '<th class="num">Итого</th><th class="num">Доля</th></tr></thead><tbody>' +
|
||
tableRows + totalRow + '</tbody></table></div>' +
|
||
'</div>' +
|
||
'<div class="kt-ai-actions">' +
|
||
'<button class="kt-ai-btn" data-variant="ghost" onclick="window.print()">Печать / экспортировать в PDF</button>' +
|
||
' '<span style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">Итого контрагентов: ' + names.length + ' · записей: ' + rows.length +
|
||
(demoOn ? " · пример данных" : "") + '</span>' +
|
||
'</div>';
|
||
document.getElementById("demoClear").style.display = demoOn ? "" : "none";
|
||
}
|
||
|
||
document.getElementById("drop").addEventListener("dragover", function (e) { e.preventDefault(); this.classList.add("over"); });
|
||
document.getElementById("drop").addEventListener("dragleave", function () { this.classList.remove("over"); });
|
||
document.getElementById("drop").addEventListener("drop", function (e) {
|
||
e.preventDefault(); this.classList.remove("over");
|
||
if (e.dataTransfer && e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
|
||
});
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|