"use strict"; const B = 1e9; function fmt(v) { return (v === null || v === undefined) ? "—" : Math.round(v).toLocaleString("ru-RU"); } function fmtB(v, dec) { if (v === null || v === undefined) return "—"; return (v / B).toFixed(dec || 2).replace(".", ",") + " млрд"; } function fmtPct(v) { if (v === null || v === undefined || !isFinite(v)) return "—"; return (v > 0 ? "+" : "") + v.toFixed(1).replace(".", ",") + " %"; } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function icon(name) { return ''; } function tip(title, rows) { let h = '' + esc(title) + ""; if (rows && rows.length) { h += '
' + rows.map((r) => "
" + esc(r[0]) + "
" + esc(r[1]) + "
" ).join("") + "
"; } return h; } let DATA = null; async function init() { const res = await fetch("data.json", { cache: "no-store" }); if (!res.ok) throw new Error("data.json: " + res.status); DATA = await res.json(); renderKPI(); renderYearChart(); renderDev25(); renderDev26(); renderNominal(); renderMain(""); renderConclusions(); } function totals() { const rows = DATA.rows; const s = (k) => rows.reduce((a, r) => a + (r[k] || 0), 0); const c25 = rows.filter((r) => r.y2025 != null).length; const c26 = rows.filter((r) => r.q1_2026 != null).length; return { t23: s("y2023"), t24: s("y2024"), t25: s("y2025"), q26: s("q1_2026"), c25, c26 }; } function renderKPI() { const t = totals(); const dYo = (t.t25 - t.t24) / t.t24 * 100; const dQ = (t.q26 - t.t25 / 2) / (t.t25 / 2) * 100; const cells = [ { lbl: "Всего за 2025 факт", num: fmtB(t.t25), ic: dYo < 0 ? "arrow-left" : "arrow-right", cls: dYo < 0 ? "down" : "up", note: fmtPct(dYo) + " к 2024" }, { lbl: "1пг 2026 факт", num: fmtB(t.q26), ic: dQ < 0 ? "arrow-left" : "arrow-right", cls: dQ < 0 ? "down" : "up", note: fmtPct(dQ) + " к 1пг 2025" }, { lbl: "2024 факт (пик)", num: fmtB(t.t24), ic: "scale", cls: "flat", note: "максимум за 3 года" }, { lbl: "Позиций с фактом 2025 / 1пг 2026", num: t.c25 + " / " + t.c26, ic: "check", cls: "flat", note: "всего в файле 103" } ]; document.getElementById("kpiBlock").innerHTML = cells.map((c) => '
' + esc(c.lbl) + "" + '' + esc(c.num) + "" + '' + icon(c.ic) + "" + esc(c.note) + "
" ).join(""); } function renderYearChart() { const svg = document.getElementById("yearChart"); const t = totals(); const items = [ { l: "2023", v: t.t23, tip: [fmtB(t.t23, 3)] }, { l: "2024", v: t.t24, tip: [fmtB(t.t24, 3), "пик за период"] }, { l: "2025", v: t.t25, tip: [fmtB(t.t25, 3), fmtPct((t.t25 - t.t24) / t.t24 * 100) + " к 2024"] }, { l: "1пг 2026", v: t.q26, tip: [fmtB(t.q26, 3), fmtPct((t.q26 - t.t25 / 2) / (t.t25 / 2) * 100) + " к 1пг 2025"] }, { l: "2026 (проект.)", v: t.q26 * 2, muted: true, tip: [fmtB(t.q26 * 2, 3), "1пг 2026 × 2 (темп)"] } ]; const W = 720, H = 260, padL = 10, padR = 10, padT = 28, padB = 40; const max = Math.max(...items.map((i) => i.v)) * 1.08; const plotW = W - padL - padR, plotH = H - padT - padB; const n = items.length; const slot = plotW / n; const barW = Math.min(slot * 0.52, 84); let s = ""; for (let i = 0; i <= 4; i++) { const y = padT + plotH - (plotH * i) / 4; s += ''; s += '' + ((max * i) / 4 / B).toFixed(1).replace(".", ",") + ""; } items.forEach((it, i) => { const x = padL + slot * i + (slot - barW) / 2; const h = plotH * (it.v / max); const y = padT + plotH - h; s += '' + '' + '' + (it.v / B).toFixed(2).replace(".", ",") + "" + "" + esc(it.l) + "" + ""; }); svg.innerHTML = s; } function renderDev25() { const out = []; for (const r of DATA.rows) { if (r.d25 != null && Math.abs(r.d25) >= 1e8) out.push(r); } out.sort((a, b) => Math.abs(b.d25) - Math.abs(a.d25)); const arr = out.slice(0, 10); const maxAbs = Math.max.apply(null, arr.map((r) => Math.abs(r.d25))) || 1; const tb = document.querySelector("#dev25 tbody"); tb.innerHTML = arr.map((r) => { const cls = r.d25 > 0 ? "pos" : "neg"; return "" + '' + esc(r.name) + "" + '' + fmt(r.y2024) + "" + '' + fmt(r.y2025) + "" + '' + fmt(r.d25) + ' (' + fmtPct(r.p25) + ")" + '' + fmtPct(r.p25) + "" + '' + ""; }).join(""); } function renderDev26() { const out = []; for (const r of DATA.rows) { if (r.q1_2026 != null && r.y2025 != null && Math.abs(r.d26) >= 5e7) out.push(r); } out.sort((a, b) => Math.abs(b.d26) - Math.abs(a.d26)); const known = out.slice(0, 8); document.querySelector("#dev26 tbody").innerHTML = known.map((r) => { const cls = r.d26 > 0 ? "pos" : "neg"; return "" + '' + esc(r.name) + 'ориентир 2025/2' + '' + fmt(r.y2025 / 2) + "" + '' + fmt(r.q1_2026) + "" + '' + fmt(r.d26) + "" + '' + fmtPct(r.p26) + "" + ""; }).join("") || 'Нет позиций с отклонением'; const newArr = DATA.rows.filter((r) => r.q1_2026 != null && r.y2025 == null) .sort((a, b) => b.q1_2026 - a.q1_2026); document.getElementById("new26").innerHTML = newArr.map((r) => "" + esc(r.name) + '' + fmt(r.q1_2026) + "" ).join(""); } function renderNominal() { const new25 = DATA.rows.filter((r) => r.y2025 != null && r.y2024 == null).sort((a, b) => b.y2025 - a.y2025); const gone25 = DATA.rows.filter((r) => r.y2024 != null && r.y2025 == null).sort((a, b) => b.y2024 - a.y2024); document.getElementById("new25").innerHTML = new25.slice(0, 8).map((r) => "" + esc(r.name) + '' + fmt(r.y2025) + "" ).join("") + "и ещё " + (new25.length - 8) + " поз." + fmt(new25.slice(8).reduce((a, r) => a + r.y2025, 0)) + ""; document.getElementById("gone25").innerHTML = gone25.slice(0, 8).map((r) => "" + esc(r.name) + '' + fmt(r.y2024) + "" ).join("") + "и ещё " + (gone25.length - 8) + " поз." + fmt(gone25.slice(8).reduce((a, r) => a + r.y2024, 0)) + ""; } function renderMain(query) { const q = query.trim().toLowerCase(); const rows = DATA.rows.filter((r) => !q || r.name.toLowerCase().includes(q)); const tb = document.querySelector("#mainTable tbody"); tb.innerHTML = rows.map((r) => { const d25cls = r.d25 == null ? "dim" : (r.d25 > 0 ? "pos" : "neg"); const d25txt = r.d25 == null ? "—" : fmt(r.d25) + (r.p25 != null ? " (" + fmtPct(r.p25) + ")" : " (новая)"); return "" + '' + esc(r.name) + "" + '' + fmt(r.y2023) + "" + '' + fmt(r.y2024) + "" + '' + fmt(r.y2025) + "" + '' + d25txt + "" + '' + fmt(r.q1_2026) + "" + ""; }).join("") || 'Ничего не найдено'; const t = totals(); document.querySelector("#mainTable tfoot").innerHTML = "Итого по файлу" + fmt(t.t23) + "" + fmt(t.t24) + "" + fmt(t.t25) + "" + fmt(t.q26) + ""; document.getElementById("rowCount").textContent = (q ? "Найдено: " + rows.length : "Всего позиций: " + rows.length) + " из 103; итоговая строка — по всем позициям файла."; } function renderConclusions() { const t = totals(); const items = [ { c: "warn", ic: "arrow-left", h: "Тренд на 3 года: 9,58 → 10,83 → 8,35 млрд тг. Рост 2024 (+13,1%) полностью отыгран падением 2025 (−" + fmtB(t.t24 - t.t25, 2) + " тг; прирост 2024 был только " + fmtB(t.t24 - t.t23, 2) + " тг). Падение 2025 — сильнее роста 2024, поэтому оно не объясняется экономией." }, { c: "risk", ic: "alert", h: "Спад 2025 на −2,48 млрд тг — эффект номенклатуры, а не экономии: 5,14 млрд тг статей 2024 «исчезли» (FLM Expenses 3,48 млрд, Prочие 0,50 млрд и др.), а «появились» 3,6 млрд тг новых. Пример переноса: «FLM Expenses» (3,48 млрд в 2024) → «FLM» (3,23 млрд в 2025, 2,18 млрд за 1пг 2026). Внутри FLM рост: +35,1% к 1пг 2025 (+568 млн тг)." }, { c: "risk", ic: "alert", h: "1пг 2026: 4,227 млрд тг — на 52 млн тг (1,3%) выше темпа 2025. Драйверы: новая статья «Ericsson (общий новый договор)» 0,827 млрд (объединены CS part и 5G part), Oracle HW 376,0 млн; ускорение Checkpoint Diamond (+64,9 млн к темпу) и MS EA (+15,7 млн). Сокращения (CORM-Ронекс −26,9 млн, Aitu Passport −21,9 млн, Nexign −68 млн) почти их компенсируют." }, { c: "warn", ic: "alert", h: "«Серверные лицензии MS EA» (базовая строка): 630 млн тг за 1пг 2026 — ровно как за весь 2025. При сохранении темпа год даст ~1,26 млрд, то есть ~+100% по этой строке. Строки «3 транш / COGS» (513 млн) и «3 транш / G&A» (45 млн) в 2026 не фигурируют — сверить периметр." }, { c: "ok", ic: "check-circle", h: "Стабильные сокращения по ключевым статьям: Ericsson CS part −695 млн в 2025 (−72%), 5G part −174 млн (−56%), а суммарно поддержка Ericsson упала с 1,186 млрд (2024) до 790 млн (2025) — минус 33%; при этом 5G-объём перешёл в «общий новый договор» 2026." }, { c: "info", ic: "scale", h: "Номенклатура: 103 позиции. 19 впервые в 2025 (3,6 млрд тг), 4 впервые в 1пг 2026 (1,276 млрд тг). Прямое сравнение «по строке» требует сверки периметра договоров — часть отклонений на графике это переносы, а не рост/падение." } ]; document.getElementById("conclusions").innerHTML = items.map((i) => '
  • ' + icon(i.ic) + "" + i.h + "
  • " ).join(""); } function boot() { init().catch((e) => { const el = document.querySelector(".page-intro .sub"); if (el) el.innerHTML = 'Не удалось загрузить данные (data.json). ' + esc(e.message) + ""; console.error(e); }); document.getElementById("tblSearch").addEventListener("input", (e) => renderMain(e.target.value)); } if (typeof document !== "undefined") { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } }