v2: презентация «Достопримечательности Алматы» — 9 слайдов с описаниями и картинками

This commit is contained in:
shumilina_a 2026-09-18 10:21:11 +00:00
parent a9f9ba61a0
commit ab7d0fefdb
10 changed files with 111 additions and 385 deletions

BIN
img/almaty-mountains.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
img/almaty-zoo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
img/botanical-garden.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
img/central-bazaar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
img/fallback.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
img/ken-as-cable.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
img/memorial-spire.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
img/money-bridge.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
img/opera-theater.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -3,405 +3,131 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Безналичные поступления — динамика за 3 года</title> <title>Алматы — достопримечательности</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css"> <script src="https://cdn.jsdelivr.net/npm/pptxgenjs@3.12.0/dist/pptxgen.bundle.js"></script>
<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> <style>
.kt-ai-app { min-height: 100vh; display: flex; flex-direction: column; } * { margin: 0; box-sizing: border-box; }
.kt-ai-app-top { position: sticky; top: 0; z-index: 5; } :root { --bg:#0f1218; --card:#171a21; --fg:#f4f4f5; --muted:#a1a1aa; --acc:#00b8cc; --acc2:#f4b942; }
.kt-ai-app-main { flex: 1; padding: var(--kt-ai-space-7) 0 var(--kt-ai-space-9); } body { font-family: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; background: var(--bg); color: var(--fg); }
.kt-ai-drop { .slide { display: none; min-height: 100vh; padding: 7vh 9vw; flex-direction: column; justify-content: center; }
border: 2px dashed var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); .slide.on { display: flex; }
padding: var(--kt-ai-space-8) var(--kt-ai-space-7); text-align: center; .slide.odd { background: linear-gradient(160deg, #0f1218 0%, #151b26 100%); }
cursor: pointer; transition: border-color .15s, background .15s; .eyebrow { color: var(--acc2); font-size: clamp(12px, 1.4vw, 15px); font-weight: 700; text-transform: uppercase; letter-spacing: .18em; margin-bottom: 14px; }
} .slide h1 { font-size: clamp(30px, 5.2vw, 58px); line-height: 1.15; font-weight: 800; letter-spacing: -.01em; margin-bottom: 24px; }
.kt-ai-drop:hover, .kt-ai-drop.over { border-color: var(--kt-ai-primary); background: var(--kt-ai-bg-soft); } .title-slide h1 { font-size: clamp(40px, 8vw, 84px); }
.kt-ai-drop-ic { display: inline-flex; margin-bottom: var(--kt-ai-space-3); color: var(--kt-ai-primary); } .subtitle { color: var(--muted); font-size: clamp(16px, 2.2vw, 24px); line-height: 1.5; max-width: 22em; margin-bottom: 30px; }
.kt-ai-drop-ic svg { width: 34px; height: 34px; } .meta { margin-top: auto; padding-top: 40px; color: var(--muted); font-size: clamp(13px, 1.6vw, 17px); letter-spacing: .04em; line-height: 1.7; }
.kt-ai-drop-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); display: block; margin-bottom: 4px; } .split { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(24px, 5vw, 64px); align-items: center; flex: 1; }
.kt-ai-drop-hint { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); display: block; line-height: 1.5; } @media (max-width: 900px) { .split { grid-template-columns: 1fr; } .photo img { height: 300px; } }
.kt-ai-files { display: flex; flex-wrap: wrap; gap: var(--kt-ai-space-2); margin-top: var(--kt-ai-space-4); } .split .text { order: 1; display: flex; flex-direction: column; justify-content: center; }
.kt-ai-filechip { .photo-wrap { order: 2; display: flex; align-items: center; }
display: inline-flex; align-items: center; gap: var(--kt-ai-space-2); .slide.odd .photo-wrap { order: 1; }
background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); .photo { position: relative; width: 100%; border-radius: 20px; overflow: hidden; box-shadow: 0 24px 60px rgba(0,0,0,.5); background: linear-gradient(135deg, #1e2530, #131a24); }
border-radius: var(--kt-ai-radius-full); padding: 5px 8px 5px 12px; .photo img { display: block; width: 100%; height: clamp(260px, 44vh, 480px); object-fit: cover; }
font-size: var(--kt-ai-text-sm); font-weight: var(--kt-ai-weight-medium); .photo .ph-cap { position: absolute; left: 0; right: 0; bottom: 0; padding: 14px 18px; font-size: 14px; color: #e4e4e7; background: linear-gradient(transparent, rgba(10,12,16,.85)); }
} .photo .ph-ic { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: rgba(0,184,204,.25); font-size: 70px; font-weight: 800; letter-spacing: .08em; text-align: center; }
.kt-ai-filechip button { ul.facts { list-style: none; }
border: 0; background: transparent; color: var(--kt-ai-fg-muted); cursor: pointer; .facts li { font-size: clamp(15px, 1.9vw, 20px); line-height: 1.5; margin-bottom: 12px; padding-left: 30px; position: relative; color: #d4d4d8; }
display: inline-flex; padding: 2px; border-radius: 50%; line-height: 0; .facts li::before { content: ""; position: absolute; left: 0; top: .45em; width: 13px; height: 13px; border-radius: 4px; background: var(--acc); }
} .num { position: fixed; bottom: 16px; right: 22px; color: #8a8a92; font-size: 14px; letter-spacing: .05em; }
.kt-ai-filechip button:hover { color: var(--kt-ai-danger); } .nav { position: fixed; bottom: 12px; left: 20px; display: flex; gap: 8px; z-index: 10; }
.kt-ai-filechip svg { width: 14px; height: 14px; } .nav button { border: 1px solid #333; background: #171719; color: #f4f4f5; border-radius: 9px; padding: 9px 15px; cursor: pointer; font-size: 14px; font-family: inherit; }
.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); } .nav button:hover { border-color: var(--acc); }
.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); } .nav .pptx { border-color: var(--acc); color: #9beeff; }
.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); } @media (max-width: 700px) { .nav button { padding: 8px 10px; font-size: 12px; } .num { display: none; } }
.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 { @media print {
.kt-ai-app-top, .kt-ai-drop, .kt-ai-actions { display: none; } .nav, .num { display: none; }
.kt-ai-app-main { padding: 0; } .slide { display: flex; page-break-after: always; min-height: 100vh; padding: 10vh 10vw; }
.kt-ai-charts { grid-template-columns: 1fr; }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="kt-ai-app"> <div id="deck"></div>
<div class="kt-ai-app-top"> <div class="nav">
<header class="kt-ai-pagehead"> <button onclick="go(-1)">&larr; Назад</button>
<div class="kt-ai-wrap kt-ai-pagehead-in"> <button onclick="go(1)">Вперёд &rarr;</button>
<strong>Безналичные поступления</strong> <button class="pptx" onclick="downloadPptx()">Скачать .pptx</button>
<span>Динамика за 3 года · сравнение</span> <button onclick="window.print()">Печать / PDF</button>
<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> </div>
</header> <div class="num" id="num"></div>
</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> <script>
var YEAR_COLORS = ["8B95A5", "3E9BBF", "00B8CC"]; var SLIDES = [
var COUNTERPARTY_COLORS = ["00B8CC", "F4B942", "7C6FE8", "4CC38A", "F472A0", "5B8DEF", "D96C4F", "8A8A92"]; { title: "Алматы — горный город", bullets: ["Седьмая столица Казахстана и самый южный крупный город страны, в двух шагах от горного хребта Заилийский Алатау", "Где горы встречаются с небоскрёбами: современный мегаполис, окружённый снежными пиками"] },
var files = []; { title: "Кен Аса", bullets: ["Смотровая площадка на высоте 2 265 м — самая высокая точка города", "Туда ведёт канатная дорога прямо из центра, второй канат поднимает ещё выше, к подножию ледников", "Отсюда вид на весь Алматы: от многоэтажек мегаполиса до снежных вершин Алатау", "Летом — тропы, велосипед и чистый горный воздух, зимой — горнолыжный склон"] },
var rows = []; { title: "Денежный мост", bullets: ["Мост через реку Алматы, соединяющий центр города с микрорайоном Бостандык", "Длиной 1 462 м — один из самых длинных мостов в Казахстане", "Вечером мост подсвечивают: отражение фонарей в воде — классика фотосессий в Алматы", "Рядом спускается канатная дорога на Кен Асу — удобно начать вечернюю прогулку"] },
var demoOn = false; { title: "Мемориальный комплекс «Астанай-Бел»", bullets: ["Парк памяти жертвам массовых репрессий и воинам, погибшим в локальных войнах", "В центре — 43-метровый шпиль из стекла: днём светится, ночью подсветка меняется по цветам", "Ухоженная территория: сквер, фонтан и мемориальная стена с именами", "Место для спокойного вечера без суеты центра"] },
{ title: "Оперный театр имени Абай", bullets: ["Главная сцена страны: здание построено в 2016 году в современном стиле", "Рядом — площадь Абая и фонтаны, одна из старейших площадей города", "Опера, балет, драматические спектакли и концерты симфонического оркестра", "Ночью facade здания эффектно подсвечивается — красиво видно с площади"] },
var DEMO = [ { title: "Центральный рынок", bullets: ["Крупнейший продуктовый рынок Алматы — один из крупнейших в Центральной Азии", "Тысячи торговых мест: фермерские продукты, орехи, сухофрукты, специи и мёд", "Отдельные дворы посвящены юфти, шубам и платкам — здесь берут сувениры", "Приезжайте на голодный желудок: тут же чай, шашлык и наливки"] },
{ year: 2023, name: "KT Bank", amount: 12500000 }, { title: "Алматинский зоопарк", bullets: ["Один из крупнейших зоопарков СНГ: более 300 видов животных на 24 гектарах", "Белые тигры, снежные барсы, львы, жирафы и пингвины", "Рядом — Ботанический сад и аттракционы для детей", "Идеально на полдня: животные, растения и прогулка в одном месте"] },
{ year: 2023, name: "Halyk Pay", amount: 8400000 }, { title: "Ботанический сад", bullets: ["Три тысячи видов растений: орхидеи, пальмы, экзотические деревья", "Девять залов, повторяющих природные зоны — от тропиков до высокогорья", "Тихое место в центре города: прогулка вдоль водоёма и фотосессия", "После садов — кафе с видом на горный хребет"] },
{ year: 2023, name: "Kaspi Online", amount: 15200000 }, { title: "Итоги: как прожить один день", bullets: ["Утро — Центральный рынок за сувенирами и завтраком, прогулка по Денежному мосту", "День — канатная дорога на Кен Асу, 2-3 часа на горе с видами", "Вечер — площадь Абая, Оперный театр, ужин с видом на горы", "Эта презентация: скачайте .pptx или распечатайте в PDF (Ctrl+P) и возьмите с собой"] }
{ 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 }
]; ];
var PHOTOS = {
function loadDemo() { 0: { seed: "almaty-mountains", cap: "Алматы и горный хребет Заилийский Алатау" },
if (demoOn) return; 1: { seed: "ken-as-cable", cap: "Канатная дорога на Кен Аса" },
demoOn = true; 2: { seed: "money-bridge", cap: "Денежный мост в вечерней подсветке" },
rows = rows.concat(DEMO); 3: { seed: "memorial-spire", cap: "Шпиль мемориального комплекса Астана-Бел" },
render(); 4: { seed: "opera-theater", cap: "Оперный театр имени Абай" },
} 5: { seed: "central-bazaar", cap: "Центральный рынок" },
6: { seed: "almaty-zoo", cap: "Алматинский зоопарк" },
function clearDemo() { 7: { seed: "botanical-garden", cap: "Ботанический сад" }
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 cur = 0;
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() { function render() {
renderFileList(); var deck = document.getElementById("deck");
var el = document.getElementById("results"); deck.innerHTML = SLIDES.map(function (s, i) {
if (!rows.length) { var ph = PHOTOS[i];
el.innerHTML = '<div class="kt-ai-empty"><svg class="kt-icon" aria-hidden="true"><use href="#chart"></use></svg><div>Загрузите файлы с данными — здесь появятся статистика, графики и таблица контрагентов.</div></div>'; if (i === 0) {
return '<section class="slide title-slide' + (i === cur ? " on" : "") + '">' +
'<div class="eyebrow">Презентация · Казахстан</div>' +
'<h1>' + s.title + '</h1>' +
'<div class="subtitle">' + s.bullets[0] + '</div>' +
'<div class="meta">' + s.bullets.slice(1).join(" · ") + '</div>' +
'</section>';
}
var photo = '<div class="photo"><div class="ph-ic">' + s.title.split(" ")[0].substring(0, 2).toUpperCase() + '</div><img src="img/' + ph.seed + '.jpg" onerror="this.style.display=\'none\'" alt="' + s.title + '" loading="lazy"><span class="ph-cap">' + ph.cap + '</span></div>';
var cls = "slide" + (i % 2 === 1 ? " odd" : "") + (i === cur ? " on" : "");
return '<section class="' + cls + '">' +
'<div class="split"><div class="text">' +
'<div class="eyebrow">Алматы · место ' + i + ' из ' + (SLIDES.length - 1) + '</div>' +
'<h1>' + s.title + '</h1>' +
'<ul class="facts">' + s.bullets.map(function (b) { return "<li>" + b + "</li>"; }).join("") + '</ul>' +
'</div><div class="photo-wrap">' + photo + '</div></div>' +
'</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 === " " || e.key === "PageDown") go(1);
if (e.key === "ArrowLeft" || e.key === "PageUp") 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, i) {
var sl = p.addSlide();
sl.background = { color: "0F1218" };
if (i === 0) {
sl.addText("Презентация · Казахстан", { x: 0.7, y: 0.9, w: 12, h: 0.4, fontSize: 13, color: "F4B942", charSpacing: 3 });
sl.addText(s.title, { x: 0.7, y: 1.5, w: 12, h: 2.2, fontSize: 44, bold: true, color: "F4F4F5" });
sl.addText(s.bullets.join("\n"), { x: 0.7, y: 4.0, w: 12, h: 2.6, fontSize: 18, color: "A1A1AA", lineSpacingMultiple: 1.4 });
return; return;
} }
var d = aggregate(); var ph = PHOTOS[i];
var years = d.years; sl.addText("Алматы · место " + i + " из " + (SLIDES.length - 1), { x: 0.7, y: 0.5, w: 12, h: 0.4, fontSize: 11, color: "F4B942", charSpacing: 2 });
var total = 0; try {
years.forEach(function (y) { total += d.byYear[y]; }); sl.addImage({ path: "img/" + ph.seed + ".jpg", x: 7.3, y: 0.5, w: 5.5, h: 6.5, sizing: { type: "cover", w: 5.5, h: 6.5 } });
var firstY = years[0], lastY = years[years.length - 1]; } catch (e) {}
var delta = d.byYear[firstY] ? ((d.byYear[lastY] - d.byYear[firstY]) / d.byYear[firstY] * 100) : null; sl.addText(s.title, { x: 0.7, y: 1.0, w: 6.3, h: 1.4, fontSize: 30, bold: true, color: "F4F4F5" });
var names = []; sl.addText(s.bullets.map(function (b) { return { text: b, options: { bullet: true, breakLine: true } }; }),
d.byYearName.forEach(function (m) { Object.keys(m).forEach(function (n) { if (names.indexOf(n) < 0) names.push(n); }); }); { x: 0.7, y: 2.6, w: 6.3, h: 4.4, fontSize: 15, color: "E4E4E7", lineSpacingMultiple: 1.35, paraSpaceAfter: 8 });
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);
}); });
p.writeFile({ fileName: "almaty-dostoprimechatelnosti.pptx" });
}
render(); render();
</script> </script>
</body> </body>