add presentation.html (10 slides, pptxgenjs, dark palette from ОДС_для_ПП) + server route /presentation.html

This commit is contained in:
ruszhum 2026-09-27 16:59:32 +00:00
parent ad4a76b42d
commit dd9fc2f758
4 changed files with 495 additions and 1 deletions

View File

@ -4,7 +4,7 @@
<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>Анализ расходов по сервисным контрактам</title> <title>Анализ расходов по сервисным контрактам — таблица</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css"> <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-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css"> <link rel="stylesheet" href="design-system/kt-ai-components.css">
@ -97,6 +97,19 @@
<div class="kpi-block" id="kpiBlock"></div> <div class="kpi-block" id="kpiBlock"></div>
<div style="margin-top:var(--kt-ai-space-6); padding:var(--kt-ai-space-5) var(--kt-ai-space-6); border:1px dashed var(--kt-ai-border-strong); border-radius:var(--kt-ai-radius-xl); background:var(--kt-ai-bg-sunken); font-size:var(--kt-ai-text-sm); color:var(--kt-ai-fg); display:flex; flex-wrap:wrap; gap:var(--kt-ai-space-3); align-items:center">
<strong>Скачать отчёт:</strong>
<a class="kt-ai-btn" data-variant="primary" data-size="sm" href="Отчёт_анализ_расходов_2023-1пг26.xlsx" download>
<svg style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#download"></use></svg>
Отчёт_анализ_расходов_2023-1пг26.xlsx
</a>
<a class="kt-ai-btn" data-variant="ghost" data-size="sm" href="presentation.html">
<svg style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"><use href="design-system/icons/kt-ai-lucide-sprite.svg#external"></use></svg>
Показать презентацию
</a>
<span class="kt-ai-fg-faint" style="font-size:var(--kt-ai-text-xs)">По кнопке «Опубликовать» откроется таблица; презентация живёт на отдельном URL — её можно открыть в другом окне.</span>
</div>
<section class="anal-block"> <section class="anal-block">
<h2>Динамика расходов по годам</h2> <h2>Динамика расходов по годам</h2>
<p class="hint">1пг 2026 — первые шесть месяцев; для сравнения с полными годами добавлен пунктирный столбец — экстраполированный год (1пг × 2).</p> <p class="hint">1пг 2026 — первые шесть месяцев; для сравнения с полными годами добавлен пунктирный столбец — экстраполированный год (1пг × 2).</p>

477
presentation.html Normal file
View File

@ -0,0 +1,477 @@
<!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>
<style>
* { margin: 0; box-sizing: border-box; }
body { font-family: "Segoe UI", "Roboto", "Helvetica Neue", -apple-system, sans-serif; background: #0D1B2A; color: #F0F4F8; }
.slide { display: none; min-height: 100vh; padding: 7vh 8vw; flex-direction: column; justify-content: flex-start; }
.slide.on { display: flex; }
.slide h1 { font-size: clamp(28px, 4.6vw, 54px); margin-bottom: 20px; line-height: 1.15; letter-spacing: -.01em; font-weight: 600; color: #F0F4F8; }
.slide .accent { color: #8EAEC8; }
.slide .kpi-row { display: flex; gap: 14px; flex-wrap: wrap; margin: 18px 0 14px; }
.slide .kpi { flex: 1 1 180px; background: #142433; border: 1px solid #1B4F72; border-radius: 10px; padding: 14px 16px; min-width: 160px; }
.slide .kpi .num { font-size: clamp(22px, 3.4vw, 36px); font-weight: 700; color: #8EAEC8; letter-spacing: -.02em; font-variant-numeric: tabular-nums; line-height: 1; }
.slide .kpi .lbl { font-size: clamp(11px, 1.4vw, 14px); color: #8899A6; margin-top: 6px; line-height: 1.35; }
.slide .kpi.warn .num { color: #D4680A; }
.slide .kpi.risk .num { color: #C0392B; }
.slide .kpi.ok .num { color: #1A7F4B; }
.slide ul { list-style: none; margin: 14px 0; display: flex; flex-direction: column; gap: 12px; }
.slide li { font-size: clamp(15px, 2vw, 22px); line-height: 1.55; padding-left: 28px; position: relative; color: #F0F4F8; }
.slide li::before { content: ""; position: absolute; left: 0; top: .55em; width: 10px; height: 10px; border-radius: 3px; background: #8EAEC8; }
.slide li::after { display: none; }
.slide li.sub { font-size: clamp(12px, 1.5vw, 16px); color: #8899A6; padding-left: 46px; line-height: 1.5; }
.slide li.sub::before { width: 6px; height: 6px; border-radius: 50%; background: #3A5A70; top: .65em; left: 18px; }
.slide table { width: 100%; border-collapse: collapse; font-size: clamp(11px, 1.5vw, 15px); margin-top: 10px; }
.slide th { text-align: left; padding: 8px 10px; background: #142433; color: #8899A6; font-weight: 600; font-size: clamp(10px, 1.3vw, 13px); text-transform: uppercase; letter-spacing: .04em; border-bottom: 1px solid #1B4F72; }
.slide th.ta-r { text-align: right; }
.slide td { padding: 8px 10px; border-bottom: 1px solid #1E3A52; color: #E8F0F8; font-variant-numeric: tabular-nums; vertical-align: top; }
.slide td.ta-r { text-align: right; }
.slide .pos { color: #C0392B; font-weight: 600; }
.slide .neg { color: #1A7F4B; font-weight: 600; }
.slide .dim { color: #6C8FA8; }
.slide .note { font-size: clamp(11px, 1.4vw, 14px); color: #8899A6; margin-top: 14px; line-height: 1.45; }
.slide .footer { margin-top: auto; padding-top: 18px; font-size: 12px; color: #6C8FA8; display: flex; gap: 18px; flex-wrap: wrap; }
.slide .footer .sep { width: 4px; height: 4px; border-radius: 50%; background: #3A5A70; align-self: center; }
.num-page { position: fixed; bottom: 18px; right: 22px; color: #6C8FA8; font-size: 13px; font-variant-numeric: tabular-nums; }
.nav { position: fixed; bottom: 14px; left: 22px; display: flex; gap: 8px; z-index: 10; }
.nav button { border: 1px solid #1B4F72; background: #142433; color: #F0F4F8; border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 14px; transition: background .15s; }
.nav button:hover { background: #1E3A52; }
.nav .pptx { border-color: #8EAEC8; color: #8EAEC8; }
.nav .pptx:hover { background: #1B4F72; }
.title-slide { justify-content: center; }
.title-slide h0 { font-size: clamp(36px, 6vw, 72px); font-weight: 600; line-height: 1.1; margin-bottom: 12px; letter-spacing: -.02em; }
.title-slide h2 { font-size: clamp(18px, 2.4vw, 28px); color: #8899A6; font-weight: 400; margin-bottom: 30px; line-height: 1.35; max-width: 80ch; }
.title-slide .badge { display: inline-block; background: #142433; border: 1px solid #1B4F72; color: #8EAEC8; padding: 4px 12px; border-radius: 20px; font-size: 12px; letter-spacing: .08em; text-transform: uppercase; margin-bottom: 22px; }
@media print {
.nav, .num-page { 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>
</div>
<div class="num-page" id="numPage"></div>
<script>
var SLIDES = [
// 1
{ type: 'title',
title: 'ОПТИМИЗАЦИЯ РАСХОДОВ ПО СЕРВИСНЫМ КОНТРАКТАМ',
subtitle: 'Анализ детализации 2023 – 1-е полугодие 2026 гг. · 103 позиции · отклонения и периметры',
chips: ['103 позиции', '2023 · 2024 · 2025 · 1пг 2026', '4 227 млн тг за 1пг 2026', 'Тенге, факт'] },
// 2
{ type: 'kpi', title: 'Итоговый свод',
kpi: [
{ num: '9,58', unit: 'млрд тг', lbl: 'Всего за 2023 (55 поз.)', tone: '' },
{ num: '10,83', unit: 'млрд тг', lbl: 'Всего за 2024 (54 поз.) — пик', tone: 'risk' },
{ num: '8,35', unit: 'млрд тг', lbl: 'Всего за 2025 (45 поз.)', tone: '' },
{ num: '4,227', unit: 'млрд тг', lbl: '1пг 2026 (31 поз.)', tone: 'ok' },
],
bullets: [
'2024 → 2025: падение <b class="neg">−2,48 млрд тг (−22,9 %)</b> — сильнее прироста 2024 (+13,1 %), это не экономия, а смена номенклатуры',
'1пг 2026: <b>4,227 млрд</b> — на <b class="pos">+52 млн тг (+1,3 %)</b> выше темпа 2025, т. е. при сохранении темпа год даст ≈ 8,45 млрд',
'По годам: 2026 (по темпу ×2) ≈ <b>8,45 млрд</b>, т. е. возврат на уровень 2024',
],
footnote: 'Источник: «Расходы по сервисным контрактам.xlsx», факт, 103 позиции.' },
// 3
{ type: 'table', title: 'Топ-10 отклонений: 2025 vs 2024',
hint: 'Статьи с Δ ≥ 0,1 млрд тг, где есть факт за оба года. Красное — рост, зелёное — снижение.',
table: [
['#', 'Позиция', '2024, млн тг', '2025, млн тг', 'Δ, млн тг', 'Δ %'],
['1', 'Техническая поддержка Ericsson (CS part)', '963,8', '268,9', '<span class="neg">−694,9</span>', '<span class="neg">−72,1 %</span>'],
['2', 'Техническая поддержка Ericsson (5G part)', '308,1', '134,5', '<span class="neg">−173,6</span>', '<span class="neg">−56,4 %</span>'],
['3', 'Сервис отправки микровызовов', '283,3', '98,9', '<span class="neg">−184,3</span>', '<span class="neg">−65,1 %</span>'],
['4', 'SUS подписка vEPC проект', '150,4', '116,3', '<span class="neg">−34,1</span>', '<span class="neg">−22,7 %</span>'],
['5', 'Мегакам лицензии / vPBX (базовая строка)', '40,7', '86,8', '<span class="pos">+46,1</span>', '<span class="pos">+113,2 %</span>'],
],
note: 'Помимо 5 строк с фактом за оба года — 19 новых статей 2025 (3,635 млрд тг): Ericsson 1 057, MS EA 630, MS EA COGS 513, Микровызовы 283, Veritas 257 и т. д.' },
// 4
{ type: 'table', title: '1пг 2026 vs 1пг 2025 — существующие статьи',
hint: '1пг 2025 = факт 2025 / 2 (ориентир при равномерном распределении). Отклонение ≥ 0,05 млрд тг.',
table: [
['Позиция', '1пг 2025, млн', '1пг 2026, млн', 'Δ, млн тг', 'Δ %'],
['Серверные лицензии MS EA', '315,1', '630,2', '<span class="pos">+315,1</span>', '<span class="pos">+100,0 %</span>'],
['Техническая поддержка Huawei', '53,7', '171,5', '<span class="pos">+117,8</span>', '<span class="pos">+219,5 %</span>'],
['Техническая поддержка Checkpoint Diamont', '36,5', '101,4', '<span class="pos">+64,9</span>', '<span class="pos">+177,6 %</span>'],
['Техническая поддержка Nexign', '630,2', '686,3', '<span class="pos">+56,1</span>', '<span class="pos">+8,9 %</span>'],
['СОРМ-Ронекс', '56,9', '30,0', '<span class="neg">−26,9</span>', '<span class="neg">−47,3 %</span>'],
['Aitu Passport (облако УЦ)', '80,2', '58,3', '<span class="neg">−21,9</span>', '<span class="neg">−27,3 %</span>'],
],
note: 'Существенные сокращения на фоне роста MS EA: в 1пг 2026 на 103 млн тг больше, чем по темпу 2025 — это рост именно по MS EA +15,7 млн и Checkpoint +64,9 млн.' },
// 5
{ type: 'table', title: 'Новые статьи 1пг 2026 (нет в 2025)',
hint: '4 статьи, которых не было в 2025 году. Суммарно 1 276 млн тг за 1пг 2026.',
table: [
['Позиция', '1пг 2026, млн тг', 'Комментарий'],
['Техническая поддержка Ericsson (общий новый договор)', '826,6', 'объединила CS part + 5G part'],
['Техническая поддержка Oracle HW', '376,0', 'новая строка (в 2025 была как Oracle HW 73,5)'],
['Мегакам лицензии', '49,0', 'лицензии'],
['Техническая поддержка Cisco smartnet', '25,0', 'встречный контракт'],
],
note: 'Ключевой сдвиг: Ericsson (общий новый договор) 827 млн тг за 1пг 2026 — это объединение двух старых строк, т. е. периметр сменился, а не появился новый расход.' },
// 6
{ type: 'two', title: 'Номенклатура 2025: что ушло и что пришло',
tableA: {
title: 'Появились в 2025 (нет в 2024)',
rows: [
['Ericsson', '1 057,0', ''],
['MS EA (базовая)', '630,2', ''],
['MS EA / 3 транш / COGS', '513,4', ''],
['Сервис отправки микровызовов', '283,3', ''],
['Veritas', '257,4', ''],
['SUS подписка vEPC проект', '150,4', ''],
['Huawei', '107,3', ''],
['Steering of Roaming, SMSW', '106,0', ''],
['…и ещё 11 позиций', '154,0', ''],
['Итого новых: 19 шт.', '3 635,2', ''],
]
},
tableB: {
title: 'Исчезли в 2025 (были в 2024)',
rows: [
['FLM Expenses', '3 481,0', '→ FLM (3 232 млн)'],
['Прочие', '500,8', ''],
['Video Conference System (Cisco)', '446,7', ''],
['Veritas', '132,2', ''],
['System SUPPORT of ZTE', '80,3', ''],
['Услуги тех. поддержки HP', '76,9', ''],
['Ремонт ФА3 гр. (Dell)', '69,5', ''],
['Maintenance FOCL (ВОЛС)', '55,1', ''],
['…и ещё 19 позиций', '270,5', ''],
['Итого исчезнувших: 29 шт.', '5 142,2', ''],
]
},
note: 'Чистая смена номенклатуры: пришло 3635 млн, ушло 5142 млн. Разница −1507 млн тг — это механика спада 2025.' },
// 7
{ type: 'text', title: 'Эффективный анализ статей 2025',
bullets: [
'<b>FLM: 3 481 млн (2024) → 3 232 млн (2025) → 2 184 млн (1пг 2026)</b>. В 2025 перенесено из «FLM Expenses» в «FLM».',
'Внутри FLM: 1пг 2026 vs 1пг 2025 — <b class="pos">+568 млн тг (+35,1 %)</b>. Статья растёт.',
'<b>Ericsson суммарно: 1 186 млн (2024) → 790 млн (2025)</b> — минус 33%. При этом 5G часть перешла в «общий новый договор» 2026 (827 млн за 1пг 2026).',
'<b>MS EA (базовая строка): 630 млн тг за 1пг 2026 = как за весь 2025</b>. При сохранении темпа год даст ≈ 1 260 млн (+100 %).',
'Строки «MS EA / 3 транш / COGS» (513 млн) и «G&A» (45 млн) в 2026 не фигурируют — сверить периметр договоров.',
'Сокращения в 1пг 2026: CORM-Ронекс −26,9 млн, Aitu Passport −21,9 млн, Nехign −68 млн (к темпу 2025).',
] },
// 8
{ type: 'recommendations', title: 'Рекомендации',
bullets: [
{ b: 'Приоритет 1 — сверить периметры 1пг 2026.', s: 'MS EA (базовая = как за весь 2025), Oracle HW (376 млн, новая), Ericsson (общий новый договор, 827 млн, объединение).', tone: 'risk' },
{ b: 'Приоритет 2 — консолидация Ericsson.', s: 'Объединение CS part + 5G part уже сделано в 2026. Следующий шаг — переход на гибридную модель (P1 + RMA за вендора, P2/P3 силами КТ), как в референсе ОДС.', tone: 'warn' },
{ b: 'Приоритет 3 — проверить FLM.', s: '+35,1 % к темпу 2025. 2 184 млн за 1пг 2026. Если рост продолжается — пересмотреть условия.', tone: 'warn' },
{ b: 'Приоритет 4 — зафиксировать сокращения.', s: 'CORM-Ронекс −26,9 млн, Aitu Passport −21,9 млн, Nехign −68 млн (к темпу 2025). Заложить в план 2027.', tone: 'ok' },
{ b: 'Приоритет 5 — реестр статусов.', s: 'К каждой статье: «прямой / посредник / дистрибьютор», «готов к инсорсингу / нужен вендор», P1/RMA за период.', tone: '' },
] },
// 9
{ type: 'close', title: 'Итого',
big: '−2,48 млрд тг\n(−22,9 %)\nк 2025',
bullets: [
'Падение 2025 — не экономия, а смена номенклатуры (FLM Expenses → FLM, Ericsson part → общий договор, MS EA COGS/G&A).',
'1пг 2026 показывает <b>рост +1,3 % к темпу 2025</b>: драйверы — MS EA +15,7 млн, Checkpoint +64,9 млн.',
'Главный риск 2027 — <b>MS EA</b>: 630 млн за 1пг 2026 = как за весь 2025. При сохранении темпа год ≈ +100 %.',
'Следующий шаг — сверить периметры договоров и зафиксировать реестр статусов (прямой / посредник, инсорсинг / вендор).',
],
footnote: 'Итоговый отчёт (Excel) + полная детализация 103 позиций прилагаются.' },
];
var cur = 0;
function render() {
var html = SLIDES.map(function (s, i) {
var on = i === cur ? ' on' : '';
var inner = renderSlide(s);
return '<section class="slide' + on + '"' + (s.type === 'title' || s.type === 'close' ? ' data-center="1"' : '') + '>' + inner + '</section>';
}).join('');
document.getElementById('deck').innerHTML = html;
document.getElementById('numPage').textContent = (cur + 1) + ' / ' + SLIDES.length;
}
function renderSlide(s) {
if (s.type === 'title') {
return '<span class="badge">' + esc(s.subtitle.split(' · ')[0]) + '</span>' +
'<h0>' + esc(s.title) + '</h0>' +
'<h2>' + esc(s.subtitle) + '</h2>' +
'<div class="kpi-row">' + s.chips.map(function (c) {
return '<div class="kpi"><div class="num" style="font-size:20px">' + esc(c) + '</div></div>';
}).join('') + '</div>' +
footer();
}
if (s.type === 'kpi') {
return '<div class="kpi-row">' + s.kpi.map(function (k) {
return '<div class="kpi ' + (k.tone || '') + '"><div class="num">' + esc(k.num) + ' ' + esc(k.unit || '') + '</div><div class="lbl">' + esc(k.lbl) + '</div></div>';
}).join('') + '</div>' +
'<ul>' + s.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join('') + '</ul>' +
'<div class="note">' + esc(s.footnote || '') + '</div>' + footer();
}
if (s.type === 'table') {
var th = s.table[0].map(function (h, i) {
return '<th' + (i > 0 ? ' class="ta-r"' : '') + '>' + esc(h) + '</th>';
}).join('');
var body = s.table.slice(1).map(function (row) {
return '<tr>' + row.map(function (c, i) {
return '<td' + (i > 0 ? ' class="ta-r"' : '') + '>' + c + '</td>';
}).join('') + '</tr>';
}).join('');
return '<h1>' + esc(s.title) + '</h1>' +
'<div class="note" style="margin-top:0">' + esc(s.hint || '') + '</div>' +
'<table><thead><tr>' + th + '</tr></thead><tbody>' + body + '</tbody></table>' +
(s.note ? '<div class="note">' + esc(s.note) + '</div>' : '') + footer();
}
if (s.type === 'two') {
function mini(t) {
var rows = t.rows.map(function (r) {
return '<tr><td>' + esc(r[0]) + '</td><td class="ta-r">' + esc(r[1]) + '</td><td class="ta-r dim" style="text-align:left;width:35%;font-weight:400">' + (r[2] ? esc(r[2]) : '') + '</td></tr>';
}).join('');
return '<div style="flex:1 1 45%;min-width:300px"><table><thead><tr><th>' + esc(t.title) + '</th><th class="ta-r">млн тг</th><th class="ta-r"></th></tr></thead><tbody>' + rows + '</tbody></table></div>';
}
return '<h1>' + esc(s.title) + '</h1>' +
'<div style="display:flex;gap:20px;flex-wrap:wrap;flex:1">' + mini(s.tableA) + mini(s.tableB) + '</div>' +
(s.note ? '<div class="note">' + esc(s.note) + '</div>' : '') + footer();
}
if (s.type === 'text') {
return '<h1>' + esc(s.title) + '</h1>' +
'<ul>' + s.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join('') + '</ul>' + footer();
}
if (s.type === 'recommendations') {
var items = s.bullets.map(function (b) {
var cls = b.tone === 'risk' ? 'kpi risk' : b.tone === 'warn' ? 'kpi warn' : b.tone === 'ok' ? 'kpi ok' : 'kpi';
return '<div class="' + cls + '" style="margin-bottom:10px"><div class="num" style="font-size:15px;font-weight:600;color:inherit">' + b.b + '</div><div class="lbl" style="margin-top:4px;font-size:13px">' + b.s + '</div></div>';
}).join('');
return '<h1>' + esc(s.title) + '</h1>' + items + footer();
}
if (s.type === 'close') {
return '<h1>' + esc(s.title) + '</h1>' +
'<div class="kpi-row" style="flex-direction:row;align-items:center">' +
'<div class="kpi" style="flex:0 0 260px;border-width:2px;border-color:#8EAEC8"><div class="num" style="font-size:40px;line-height:1.1;white-space:pre-wrap">' + esc(s.big) + '</div><div class="lbl">Итог 2025 vs 2024</div></div>' +
'<div style="flex:1"><ul style="margin:0">' + s.bullets.map(function (b) { return '<li>' + b + '</li>'; }).join('') + '</ul>' +
'<div class="note">' + esc(s.footnote || '') + '</div></div></div>' + footer();
}
return '';
}
function footer() {
return '<div class="footer"><span>Опти­миза­ция расхо­дов</span><span class="sep"></span><span>103 пози­ции · 2023 – 1пг 2026</span><span class="sep"></span><span>Источ­ник: Расхо­ды по сер­вис­ным кон­трак­там.xlsx</span></div>';
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
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);
});
// ============ PPTX ============
// Палитра из референса
var C = { bg: '0D1B2A', panel: '142433', border: '1B4F72', accent: '8EAEC8', mute: '8899A6', dim: '6C8FA8', ok: '1A7F4B', warn: 'D4680A', risk: 'C0392B', fg: 'F0F4F8', line: '1E3A52' };
function downloadPptx() {
var p = new PptxGenJS();
p.defineLayout({ name: 'W', width: 13.33, height: 7.5 });
p.layout = 'W';
p.author = 'Vibe42';
p.company = 'Анализ сервисных контрактов';
function addBg(sl) {
sl.background = { color: C.bg };
sl.addText('Оптимизация расходов · 103 позиции · 2023 – 1пг 2026', { x: 0.5, y: 7.0, w: 12, h: 0.4, fontSize: 9, color: C.dim, fontFace: 'Segoe UI' });
}
function addTitle(sl, text, opts) {
sl.addText(text, Object.assign({ x: 0.5, y: 0.35, w: 12, h: 0.9, fontSize: 30, bold: true, color: C.fg, fontFace: 'Segoe UI' }, opts || {}));
}
function addKpiRow(sl, items, y) {
var n = items.length;
var w = 12.4 / n;
items.forEach(function (k, i) {
var x = 0.5 + i * w;
sl.addShape(p.ShapeType.roundRect, { x: x, y: y, w: w - 0.12, h: 1.4, fill: { color: C.panel }, line: { color: C.border, width: 1 }, rectRadius: 0.06 });
var tone = k.tone === 'risk' ? C.risk : k.tone === 'warn' ? C.warn : k.tone === 'ok' ? C.ok : C.accent;
sl.addText(k.num + (k.unit ? ' ' + k.unit : ''), { x: x + 0.15, y: y + 0.15, w: w - 0.4, h: 0.6, fontSize: 24, bold: true, color: tone, fontFace: 'Segoe UI' });
sl.addText(k.lbl, { x: x + 0.15, y: y + 0.75, w: w - 0.4, h: 0.6, fontSize: 10, color: C.mute, fontFace: 'Segoe UI' });
});
}
function addBullets(sl, bullets, opts) {
sl.addText(bullets.map(function (b, idx) {
return { text: b, options: { bullet: { code: '2022', indent: 14 }, color: C.fg, breakLine: true, paraSpaceAfter: 8 } };
}), Object.assign({ x: 0.5, y: 2.0, w: 12.4, h: 4.8, fontSize: 14, fontFace: 'Segoe UI', color: C.fg }, opts || {}));
}
function addRowsTable(sl, headers, rows, opts) {
var header = headers.map(function (h) { return { text: h, options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 11, valign: 'middle' } }; });
var body = rows.map(function (row, ri) {
return row.map(function (cell, ci) {
var color = C.fg;
var bold = false;
if (typeof cell === 'string') {
if (cell.indexOf('class="pos"') >= 0 || (cell.indexOf('+') === 0 && ci > 0)) { color = C.risk; bold = true; }
if (cell.indexOf('class="neg"') >= 0 || (ci > 0 && cell.indexOf('−') === 0)) { color = C.ok; bold = true; }
cell = cell.replace(/<[^>]+>/g, '');
}
return { text: String(cell), options: { color: color, bold: bold, fill: { color: ri % 2 ? C.bg : '101D2A' }, fontSize: 11 } };
});
});
var table = [header].concat(body);
sl.addTable(table, Object.assign({ x: 0.5, y: 2.4, w: 12.4, fontFace: 'Segoe UI', rowH: 0.4, autoPage: false, valign: 'middle' }, opts || {}));
}
// 1: title
(function () {
var s = SLIDES[0];
var sl = p.addSlide(); addBg(sl);
sl.addShape(p.ShapeType.rect, { x: 0.5, y: 0.5, w: 0.35, h: 0.35, fill: { color: C.accent } });
sl.addText(s.subtitle, { x: 1.0, y: 0.5, w: 12, h: 0.5, fontSize: 12, color: C.accent, fontFace: 'Segoe UI', charSpacing: 4 });
sl.addText(s.title, { x: 0.5, y: 2.2, w: 12.4, h: 1.8, fontSize: 50, bold: true, color: C.fg, fontFace: 'Segoe UI', charSpacing: -1 });
var chips = s.chips.map(function (c) { return { text: c, options: { bullet: false, color: C.accent, breakLine: true } }; });
sl.addText(chips, { x: 0.5, y: 4.6, w: 12.4, h: 1.6, fontSize: 14, fontFace: 'Segoe UI' });
})();
// 2: KPI + bullets
(function () {
var s = SLIDES[1];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
addKpiRow(sl, s.kpi, 1.5);
sl.addText(s.bullets.map(function (b) {
return { text: b.replace(/<[^>]+>/g, ''), options: { bullet: { code: '2022', indent: 14 }, color: C.fg, breakLine: true, paraSpaceAfter: 10 } };
}), { x: 0.5, y: 3.3, w: 12.4, h: 2.8, fontSize: 14, fontFace: 'Segoe UI' });
sl.addText(s.footnote, { x: 0.5, y: 6.2, w: 12.4, h: 0.4, fontSize: 10, color: C.mute, italic: true, fontFace: 'Segoe UI' });
})();
// 3: dev25
(function () {
var s = SLIDES[2];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
sl.addText(s.hint, { x: 0.5, y: 1.3, w: 12.4, h: 0.4, fontSize: 11, color: C.mute, italic: true, fontFace: 'Segoe UI' });
var headers = s.table[0].map(function (h, i) { return { text: h, options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 11, valign: 'middle' } }; });
var rows = s.table.slice(1).map(function (row, ri) {
return row.map(function (cell, ci) {
var color = C.fg, bold = false, txt = String(cell).replace(/<[^>]+>/g, '');
if (ci === 4) { color = txt.indexOf('+') === 0 ? C.risk : C.ok; bold = true; }
if (ci === 5) { color = txt.indexOf('+') === 0 ? C.risk : C.ok; bold = true; }
if (ri % 2 === 1) return { text: txt, options: { color: color, bold: bold, fill: { color: '101D2A' }, fontSize: 11 } };
return { text: txt, options: { color: color, bold: bold, fontSize: 11 } };
});
});
sl.addTable([headers].concat(rows), { x: 0.5, y: 1.9, w: 12.4, fontFace: 'Segoe UI', rowH: 0.55, autoPage: false, valign: 'middle' });
sl.addText(s.note, { x: 0.5, y: 5.6, w: 12.4, h: 0.9, fontSize: 11, color: C.mute, fontFace: 'Segoe UI' });
})();
// 4: dev26
(function () {
var s = SLIDES[3];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
sl.addText(s.hint, { x: 0.5, y: 1.3, w: 12.4, h: 0.4, fontSize: 11, color: C.mute, italic: true, fontFace: 'Segoe UI' });
var headers = s.table[0].map(function (h, i) { return { text: h, options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 11, valign: 'middle' } }; });
var rows = s.table.slice(1).map(function (row, ri) {
return row.map(function (cell, ci) {
var color = C.fg, bold = false, txt = String(cell).replace(/<[^>]+>/g, '');
if (ci === 3 || ci === 4) { color = txt.indexOf('+') === 0 ? C.risk : C.ok; bold = true; }
if (ri % 2 === 1) return { text: txt, options: { color: color, bold: bold, fill: { color: '101D2A' }, fontSize: 11 } };
return { text: txt, options: { color: color, bold: bold, fontSize: 11 } };
});
});
sl.addTable([headers].concat(rows), { x: 0.5, y: 1.9, w: 12.4, fontFace: 'Segoe UI', rowH: 0.5, autoPage: false, valign: 'middle' });
sl.addText(s.note, { x: 0.5, y: 5.4, w: 12.4, h: 0.9, fontSize: 11, color: C.mute, fontFace: 'Segoe UI' });
})();
// 5: new26
(function () {
var s = SLIDES[4];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
sl.addText(s.hint, { x: 0.5, y: 1.3, w: 12.4, h: 0.4, fontSize: 11, color: C.mute, italic: true, fontFace: 'Segoe UI' });
var headers = s.table[0].map(function (h, i) { return { text: h, options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 11, valign: 'middle' } }; });
var rows = s.table.slice(1).map(function (row, ri) {
return row.map(function (cell, ci) {
if (ci === 1) return { text: String(cell), options: { bold: true, color: C.accent, fill: ri % 2 ? { color: '101D2A' } : undefined, fontSize: 12 } };
return { text: String(cell), options: { fill: ri % 2 ? { color: '101D2A' } : undefined, fontSize: 11 } };
});
});
sl.addTable([headers].concat(rows), { x: 0.5, y: 1.9, w: 12.4, fontFace: 'Segoe UI', rowH: 0.6, autoPage: false, valign: 'middle' });
sl.addText(s.note, { x: 0.5, y: 4.8, w: 12.4, h: 1.2, fontSize: 11, color: C.mute, fontFace: 'Segoe UI' });
})();
// 6: two
(function () {
var s = SLIDES[5];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
function mini(t, x) {
sl.addText(t.title, { x: x, y: 1.5, w: 6.0, h: 0.4, fontSize: 12, bold: true, color: C.accent, fontFace: 'Segoe UI' });
var header = [{ text: '', options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 10 } }, { text: 'млн тг', options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 10, valign: 'middle', align: 'right' } }, { text: '', options: { bold: true, color: '8899A6', fill: { color: C.panel }, fontSize: 10 } }];
var rows = t.rows.map(function (row, ri) {
var bg = ri % 2 ? { color: '101D2A' } : undefined;
var isTotal = ri === t.rows.length - 1;
return [
{ text: row[0], options: { bold: isTotal, fill: bg, fontSize: 10 } },
{ text: row[1], options: { bold: isTotal, align: 'right', fill: bg, fontSize: 10 } },
{ text: row[2] || '', options: { color: C.dim, fill: bg, fontSize: 10 } },
];
});
sl.addTable([header].concat(rows), { x: x, y: 1.95, w: 6.0, fontFace: 'Segoe UI', rowH: 0.35, autoPage: false, valign: 'middle' });
}
mini(s.tableA, 0.5);
mini(s.tableB, 6.8);
sl.addText(s.note, { x: 0.5, y: 5.8, w: 12.4, h: 0.6, fontSize: 11, color: C.mute, fontFace: 'Segoe UI' });
})();
// 7: text
(function () {
var s = SLIDES[6];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
sl.addText(s.bullets.map(function (b) {
return { text: b.replace(/<[^>]+>/g, ''), options: { bullet: { code: '2022', indent: 14 }, color: C.fg, breakLine: true, paraSpaceAfter: 12 } };
}), { x: 0.5, y: 1.7, w: 12.4, h: 4.8, fontSize: 14, fontFace: 'Segoe UI' });
})();
// 8: recommendations
(function () {
var s = SLIDES[7];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
var y = 1.55;
s.bullets.forEach(function (b) {
var tone = b.tone === 'risk' ? C.risk : b.tone === 'warn' ? C.warn : b.tone === 'ok' ? C.ok : C.accent;
sl.addShape(p.ShapeType.roundRect, { x: 0.5, y: y, w: 12.4, h: 0.85, fill: { color: C.panel }, line: { color: C.border, width: 1 }, rectRadius: 0.05 });
sl.addShape(p.ShapeType.rect, { x: 0.5, y: y, w: 0.08, h: 0.85, fill: { color: tone } });
sl.addText(b.b, { x: 0.75, y: y + 0.05, w: 12, h: 0.4, fontSize: 13, bold: true, color: C.fg, fontFace: 'Segoe UI' });
sl.addText(b.s, { x: 0.75, y: y + 0.45, w: 12, h: 0.35, fontSize: 10, color: C.mute, fontFace: 'Segoe UI' });
y += 1.0;
});
})();
// 9: close
(function () {
var s = SLIDES[8];
var sl = p.addSlide(); addBg(sl); addTitle(sl, s.title);
sl.addShape(p.ShapeType.roundRect, { x: 0.5, y: 1.7, w: 4.4, h: 4.4, fill: { color: C.panel }, line: { color: C.accent, width: 2 }, rectRadius: 0.15 });
sl.addText(s.big, { x: 0.7, y: 2.2, w: 4.0, h: 2.0, fontSize: 30, bold: true, color: C.accent, fontFace: 'Segoe UI', paraSpaceBefore: 6, lineSpacingMultiple: 1.2 });
sl.addText('Итог 2025 vs 2024', { x: 0.7, y: 4.4, w: 4.0, h: 0.6, fontSize: 12, color: C.mute, fontFace: 'Segoe UI' });
sl.addText(s.bullets.map(function (b) {
return { text: b.replace(/<[^>]+>/g, ''), options: { bullet: { code: '2022', indent: 14 }, color: C.fg, breakLine: true, paraSpaceAfter: 10 } };
}), { x: 5.3, y: 1.7, w: 7.4, h: 4.4, fontSize: 13, fontFace: 'Segoe UI' });
sl.addText(s.footnote, { x: 5.3, y: 6.1, w: 7.4, h: 0.5, fontSize: 10, color: C.mute, italic: true, fontFace: 'Segoe UI' });
})();
p.writeFile({ fileName: "Оптимизация_расходов_2023-1пг26.pptx" });
}
render();
</script>
</body>
</html>

View File

@ -24,6 +24,10 @@ const srv = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return fs.createReadStream(path.join(__dirname, "index.html")).pipe(res); return fs.createReadStream(path.join(__dirname, "index.html")).pipe(res);
} }
if (url === "/presentation.html") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return fs.createReadStream(path.join(__dirname, "presentation.html")).pipe(res);
}
if (url === "/data.json") { if (url === "/data.json") {
res.writeHead(200, { "Content-Type": "application/json" }); res.writeHead(200, { "Content-Type": "application/json" });
return fs.createReadStream(path.join(__dirname, "data.json")).pipe(res); return fs.createReadStream(path.join(__dirname, "data.json")).pipe(res);

Binary file not shown.