/* Дашборд по продажам B2B: статические данные + имитация в localStorage. */ (function () { "use strict"; /* ---------- Периоды: 8 месяцев, 2 квартала, 2 года ---------- */ var PERIODS = [ { id: "year", label: "2026", period: "Год" }, { id: "q2", label: "II кв. 2026", period: "Квартал" }, { id: "q1", label: "I кв. 2026", period: "Квартал" }, { id: "sep", label: "Сентябрь", period: "Месяц" }, { id: "aug", label: "Август", period: "Месяц" }, { id: "jul", label: "Июль", period: "Месяц" }, { id: "jun", label: "Июнь", period: "Месяц" }, { id: "may", label: "Май", period: "Месяц" }, { id: "apr", label: "Апрель", period: "Месяц" }, { id: "q4", label: "IV кв. 2025", period: "Квартал" }, { id: "q3", label: "III кв. 2025", period: "Квартал" }, { id: "q2p", label: "II кв. 2025", period: "Квартал" }, { id: "q1p", label: "I кв. 2025", period: "Квартал" }, { id: "dec", label: "Декабрь", period: "Месяц" }, { id: "nov", label: "Ноябрь", period: "Месяц" }, { id: "oct", label: "Октябрь", period: "Месяц" }, { id: "sep25", label: "Сентябрь", period: "Месяц" }, { id: "aug25", label: "Август", period: "Месяц" }, { id: "jul25", label: "Июль", period: "Месяц" }, { id: "jun25", label: "Июнь", period: "Месяц" } ]; var PERIOD_BY_ID = {}; for (var p = 0; p < PERIODS.length; p++) PERIOD_BY_ID[PERIODS[p].id] = PERIODS[p]; var MONTH_LABELS = ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек"]; /* ---------- Направления услуг ---------- */ var CATEGORIES = ["Доступ в интернет", "IP-телефония", "Транзит", "Колл-центр", "Видеонаблюдение"]; var CAT_COLORS = [ "var(--kt-ai-chart-blue)", "var(--kt-ai-chart-green)", "var(--kt-ai-chart-orange)", "var(--kt-ai-chart-pink)", "var(--kt-ai-chart-salmon)" ]; /* ---------- Клиенты (нейтральные примеры) ---------- */ var CLIENTS = [ { name: "СтройМонтаж", dir: "Алматы", dirId: "almaty" }, { name: "АгроТрейд Костанай", dir: "Костанай", dirId: "kostanay" }, { name: "Логистик Плюс", dir: "Астана", dirId: "astana" }, { name: "ПродМир", dir: "Шымкент", dirId: "shymkent" }, { name: "ТехноПолимер", dir: "Караганда", dirId: "karaganda" }, { name: "МедФарм К", dir: "Кокшетау", dirId: "kokshetau" }, { name: "Ритейл Гур", dir: "Алматы", dirId: "almaty" }, { name: "ЭкоЭнерго", dir: "Актау", dirId: "aktau" } ]; /* ---------- Услуга по направлению ---------- */ function serviceFor(category) { switch (category) { case "Доступ в интернет": return "Волоконная линия, 500 Мбит/с"; case "IP-телефония": return "Абоненты, 48 номеров"; case "Транзит": return "Транзит, 10 Гбит/с"; case "Колл-центр": return "Виртуальные операторы, 12 мест"; case "Видеонаблюдение": return "Камеры, 8 точек"; } return "Услуги связи"; } /* ---------- Менеджеры ---------- */ var MANAGERS = ["А. Сегизбаев", "Ж. Нурлан", "М. Ибраимова", "Д. Калетаев"]; /* ---------- Сумма: базовая + детерминированная поправка ---------- */ function hashStr(s) { var h = 2166136261; for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } function dealSum(seed) { var h = hashStr(seed) % 1000; var base = 320000 + (h % 40) * 27500 + Math.floor(h / 40) * 80000; return base; } /* ---------- Генерация сделок на 20 периодов ---------- */ var ALL_DEALS = []; function MONTH_ZERO(m) { return m < 10 ? "0" + m : "" + m; } function periodMonthLabel(per) { if (per.period === "Месяц") return MONTH_BY_LABEL[per.label] || "09"; return "09"; } var MONTH_BY_LABEL = { "Январь": "01", "Февраль": "02", "Март": "03", "Апрель": "04", "Май": "05", "Июнь": "06", "Июль": "07", "Август": "08", "Сентябрь": "09", "Октябрь": "10", "Ноябрь": "11", "Декабрь": "12" }; function quarterMonths(qid) { if (qid === "q1") return [1, 2, 3]; if (qid === "q2") return [4, 5, 6]; if (qid === "q3") return [7, 8, 9]; if (qid === "q4") return [10, 11, 12]; if (qid === "q1p") return [1, 2, 3]; if (qid === "q2p") return [4, 5, 6]; if (qid === "q3p") return [7, 8, 9]; if (qid === "q4p") return [10, 11, 12]; return [9]; } (function generate() { for (var pi = 0; pi < PERIODS.length; pi++) { var per = PERIODS[pi]; var year = per.id.indexOf("25") > -1 ? 2025 : 2026; var count; if (per.period === "Год") count = 76; else if (per.period === "Квартал") count = 23; else count = 9; for (var k = 0; k < count; k++) { var hc = hashStr(per.id + "#" + k); var client = CLIENTS[hc % CLIENTS.length]; var category = CATEGORIES[Math.floor(hc / 7) % CATEGORIES.length]; var manager = MANAGERS[Math.floor(hc / 13) % MANAGERS.length]; var roll = (hc >>> 4) % 20; var status = roll < 12 ? "Закрыта" : (roll < 16 ? "На согласовании" : "В работе"); var seqN = ALL_DEALS.length + 1; var date; if (status === "Закрыта") { if (per.period === "Год") { var m2 = (hc % 12) + 1; var d = ((hc >>> 2) % 26) + 1; date = year + "-" + MONTH_ZERO(m2) + "-" + MONTH_ZERO(d); } else if (per.period === "Квартал") { var qMonths = quarterMonths(per.id); var m3 = qMonths[hc % 3]; var d3 = ((hc >>> 2) % 26) + 1; date = year + "-" + MONTH_ZERO(m3) + "-" + MONTH_ZERO(d3); } } else { /* Открытые сделки показываются на срезе: дата — срез конца периода */ var y2 = yearOfPeriod(per.id); var mOpen; if (per.period === "Месяц") mOpen = periodMonthLabel(per); else mOpen = "09"; /* срез квартала: конец третьего месяца */ date = y2 + "-" + mOpen + "-25"; } ALL_DEALS.push({ id: seqN, number: "B2B-26-" + (1000 + seqN), client: client.name, dirId: client.dirId, dir: client.dir, category: category, service: serviceFor(category), manager: manager, amount: dealSum(per.id + "#" + k), status: status, date: date, period: per.id }); } } })(); function yearOfPeriod(pid) { return pid.indexOf("25") !== -1 ? 2025 : 2026; } /* ---------- Форматирование ---------- */ function fmtMoney(v) { var s = Math.round(v / 1000).toString().replace(/\B(?=(\d{3})+(?!\d))/g, "\u202f"); return s + "\u202f₸"; } function fmtMoneyShort(v) { if (v >= 1000000) { var m = v / 1000000; var s = (Math.round(m * 100) / 100).toString().replace(/\B(?=(\d{3})+(?!\d))/g, "\u202f"); return s + "\u202fmn ₸"; } return fmtMoney(v); } function fmtDate(iso) { var p = iso.split("-"); return p[2] + "." + p[1] + "." + p[0]; } function statusInfo(status) { if (status === "Закрыта") return { status: "ok", label: "Закрыта" }; if (status === "На согласовании") return { status: "warn", label: "На согласовании" }; return { status: "info", label: "В работе" }; } /* ---------- Состояние (localStorage) ---------- */ var LS_PERIOD = "b2b.period"; var LS_VIEW = "b2b.view"; var LS_DIR = "b2b.dir"; function lsGet(key, fallback) { try { var v = localStorage.getItem(key); return (v === null || v === "") ? fallback : v; } catch (e) { return fallback; } } function lsSet(key, value) { try { localStorage.setItem(key, value); } catch (e) {} } var state = { period: lsGet(LS_PERIOD, "q2"), view: lsGet(LS_VIEW, "overview"), dir: lsGet(LS_DIR, ""), search: "" }; if (!PERIOD_BY_ID[state.period]) state.period = "q2"; /* ---------- Выборка ---------- */ function dealsOf(periodId) { var out = []; for (var i = 0; i < ALL_DEALS.length; i++) { if (ALL_DEALS[i].period === periodId) out.push(ALL_DEALS[i]); } return out; } function sumDeals(list) { var s = 0; for (var i = 0; i < list.length; i++) s += list[i].amount; return s; } function closedList(list) { var out = []; for (var i = 0; i < list.length; i++) if (list[i].status === "Закрыта") out.push(list[i]); return out; } function openList(list) { var out = []; for (var i = 0; i < list.length; i++) if (list[i].status !== "Закрыта") out.push(list[i]); return out; } function applyDirFilter(list) { if (!state.dir) return list; var out = []; for (var i = 0; i < list.length; i++) if (list[i].dirId === state.dir) out.push(list[i]); return out; } /* ---------- Обзор: KPI и блоки ---------- */ function renderOverview() { var closed = closedList(dealsOf(state.period)); var deals = applyDirFilter(closed); var dealsSum = sumDeals(deals); var active = applyDirFilter(openList(dealsOf(state.period))); var avg = deals.length ? Math.round(dealsSum / deals.length) : 0; var dTexts = []; var dVals = [0, 0, 0]; for (var k = 0; k < 3; k++) { var cur = k === 0 ? dealsSum : (k === 1 ? deals.length : avg); var prevV = prevPeriodFor(k); if (prevV > 0 && prevV !== cur) { var pct = Math.round(((cur - prevV) / prevV) * 100); dVals[k] = pct; dTexts[k] = (pct >= 0 ? "+" : "−") + Math.abs(pct) + "%"; } else { dTexts[k] = ""; } } var kpis = [ { label: "Выручка, закрытые сделки", value: fmtMoney(dealsSum), hint: periodHint(), delta: dTexts[0], deltaVal: dVals[0] }, { label: "Сделок закрыто", value: String(deals.length), hint: "из " + closedList(dealsOf(state.period)).length, delta: dTexts[1], deltaVal: dVals[1] }, { label: "Средний чек", value: avg > 0 ? fmtMoney(avg) : "—", hint: "по закрытым", delta: dTexts[2], deltaVal: dVals[2] }, { label: "Открытый портфель", value: fmtMoney(sumDeals(active)), hint: active.length + " активных сделок" } ]; var html = ""; for (var i = 0; i < kpis.length; i++) { html += '
'; html += '' + kpis[i].label + ""; html += ' 10 ? ' data-long="true"' : "") + ">" + kpis[i].value + ""; html += '' + kpis[i].hint + ""; if (kpis[i].delta) html += '' + kpis[i].delta + ""; html += "
"; } document.getElementById("kpiStrip").innerHTML = html; renderTrend(); renderCategories(); renderTopClients(); } function periodHint() { var per = PERIOD_BY_ID[state.period]; return per.label + (state.dir ? " · " + dirName(state.dir) : ""); } function dirName(dirId) { for (var i = 0; i < CLIENTS.length; i++) if (CLIENTS[i].dirId === dirId) return CLIENTS[i].dir; return ""; } function closedPrevPeriod() { var idx = periodIndex(); if (idx + 1 >= PERIODS.length) return []; return closedList(dealsOf(PERIODS[idx + 1].id)); } function periodIndex() { for (var i = 0; i < PERIODS.length; i++) if (PERIODS[i].id === state.period) return i; return 0; } function prevPeriodFor(kind) { var pl = closedPrevPeriod(); if (kind === 0) return sumDeals(applyDirFilter(pl)); if (kind === 1) return applyDirFilter(pl).length; var fl = applyDirFilter(pl); return fl.length ? sumDeals(fl) / fl.length : 0; } /* ---------- График: динамика по месяцам ---------- */ function trendSlots() { var per = PERIOD_BY_ID[state.period]; if (per.period === "Год") return monthsOf(yearOfState(), 1, 12); if (per.period === "Квартал") { var qm = quarterMonths(state.period); return monthsOf(yearOfState(), qm[0], qm[2]); } var m = parseInt(periodMonthLabel(per), 10); return [{ label: MONTH_LABELS[m - 1], key: yearOfState() + "-" + MONTH_ZERO(m) }]; } function yearOfState() { if (state.period.indexOf("25") !== -1) return 2025; return 2026; } function renderTrend() { var slots = trendSlots(); for (var s = 0; s < slots.length; s++) slots[s].value = 0; var closed = closedList(dealsOf(state.period)); for (var i = 0; i < closed.length; i++) { var key = closed[i].date.slice(0, 7); for (s = 0; s < slots.length; s++) if (slots[s].key === key) slots[s].value += closed[i].amount; } var max = 0, total = 0; for (s = 0; s < slots.length; s++) { if (slots[s].value > max) max = slots[s].value; total += slots[s].value; } var bars = "", axis = ""; for (s = 0; s < slots.length; s++) { var h = max > 0 ? Math.round((slots[s].value / max) * 100) : 0; bars += '
'; axis += "" + slots[s].label + ""; } var per = PERIOD_BY_ID[state.period]; document.getElementById("trendChart").innerHTML = '
' + '
Динамика выручки по месяцам
' + '
' + fmtMoneyShort(total) + "закрытые сделки
" + '' + per.label + "
" + '
' + bars + "
" + '
' + axis + "
"; } /* ---------- По направлениям ---------- */ function renderCategories() { var closed = closedList(applyDirFilter(dealsOf(state.period))); var totals = {}; for (var c = 0; c < CATEGORIES.length; c++) totals[CATEGORIES[c]] = 0; for (var i = 0; i < closed.length; i++) totals[closed[i].category] += closed[i].amount; var order = CATEGORIES.slice(); order.sort(function (a, b) { return totals[b] - totals[a]; }); var max = 0; for (c = 0; c < order.length; c++) if (totals[order[c]] > max) max = totals[order[c]]; var rows = ""; for (c = 0; c < order.length; c++) { var cat = order[c]; var w = max > 0 ? Math.round((totals[cat] / max) * 100) : 0; rows += '
' + '' + cat + "" + '' + fmtMoneyShort(totals[cat]) + "" + '
'; } document.getElementById("categoryChart").innerHTML = '
Выручка по направлениям
' + '
' + rows + "
"; } function catColor(cat) { for (var c = 0; c < CATEGORIES.length; c++) if (CATEGORIES[c] === cat) return CAT_COLORS[c]; return "var(--kt-ai-chart-blue)"; } /* ---------- Топ клиентов ---------- */ function renderTopClients() { var closed = closedList(applyDirFilter(dealsOf(state.period))); var byClient = {}, byClientDir = {}; for (var i = 0; i < closed.length; i++) { var d = closed[i]; byClient[d.client] = (byClient[d.client] || 0) + d.amount; byClientDir[d.client] = d.dir; } var arr = []; for (var name in byClient) arr.push({ name: name, sum: byClient[name], dir: byClientDir[name] }); arr.sort(function (a, b) { return b.sum - a.sum; }); var rows = ""; for (i = 0; i < 5 && i < arr.length; i++) { rows += '
' + '' + (i + 1) + "" + '
' + arr[i].name + '
' + arr[i].dir + "
" + '' + fmtMoneyShort(arr[i].sum) + "
"; } document.getElementById("topClients").innerHTML = '
Крупнейшие клиенты периода
' + '
' + rows + "
"; } /* ---------- Детализация: фильтры и таблица ---------- */ var DIRS = [ { id: "almaty", name: "Алматы" }, { id: "astana", name: "Астана" }, { id: "karaganda", name: "Караганда" }, { id: "kostanay", name: "Костанай" }, { id: "aktau", name: "Актау" }, { id: "kokshetau", name: "Кокшетау" }, { id: "shymkent", name: "Шымкент" } ]; function renderDirChips() { var bar = document.getElementById("dirFilters"); var html = '"; for (var i = 0; i < DIRS.length; i++) { html += '"; } bar.innerHTML = html; } function renderDeals() { var all = closedList(dealsOf(state.period)); var dirList = applyDirFilter(all); var q = state.search.trim().toLowerCase(); var rows = []; for (var i = 0; i < dirList.length; i++) { var d = dirList[i]; if (q && d.client.toLowerCase().indexOf(q) === -1 && d.manager.toLowerCase().indexOf(q) === -1 && d.service.toLowerCase().indexOf(q) === -1 && d.category.toLowerCase().indexOf(q) === -1) continue; rows.push(d); } rows.sort(function (a, b) { return a.date < b.date ? 1 : -1; }); var body = document.getElementById("dealsBody"); var html = ""; var limit = 50; for (i = 0; i < rows.length && i < limit; i++) { var st = statusInfo(rows[i].status); html += "" + '' + rows[i].client + '' + rows[i].dir + "" + "" + rows[i].service + '' + rows[i].category + "" + "" + rows[i].manager + "" + '' + fmtMoney(rows[i].amount) + "" + '' + st.label + "" + ""; } body.innerHTML = html; document.getElementById("dealsEmpty").hidden = rows.length > 0; var totalSum = sumDeals(rows); document.getElementById("dealsCount").textContent = rows.length + " сделок"; document.getElementById("dealsSum").textContent = "Сумма: " + fmtMoney(totalSum); document.getElementById("dealsTable").style.display = rows.length ? "" : "none"; } /* ---------- Навигация ---------- */ function setView(view) { state.view = view; lsSet(LS_VIEW, view); document.getElementById("view-overview").hidden = view !== "overview"; document.getElementById("view-detail").hidden = view !== "detail"; var items = document.querySelectorAll(".kt-ai-nav-item[data-view]"); for (var i = 0; i < items.length; i++) { if (items[i].getAttribute("data-view") === view) items[i].classList.add("is-active"); else items[i].classList.remove("is-active"); } document.getElementById("pageTitle").textContent = view === "overview" ? "Продажи B2B" : "Детализация"; document.getElementById("crumb").textContent = view === "overview" ? "Обзор" : "Обзор · " + PERIOD_BY_ID[state.period].label; renderAll(); } function setPeriod(kind) { /* kind: month | quarter | year */ var pick = kind === "year" ? "year" : (kind === "quarter" ? "q2" : "sep"); state.period = pick; lsSet(LS_PERIOD, pick); renderAll(); } function renderAll() { renderOverview(); renderDeals(); document.getElementById("navDealCount").textContent = String(dealsOf(state.period).length); } /* ---------- События ---------- */ function bind() { var seg = document.getElementById("periodSeg"); var segBtns = seg.querySelectorAll(".seg"); for (var s = 0; s < segBtns.length; s++) { segBtns[s].addEventListener("click", function () { setPeriod(this.getAttribute("data-period")); syncPeriodSeg(); }); } var navItems = document.querySelectorAll(".kt-ai-nav-item[data-view]"); for (s = 0; s < navItems.length; s++) { navItems[s].addEventListener("click", function (e) { e.preventDefault(); setView(this.getAttribute("data-view")); if (history.replaceState) history.replaceState(null, "", "#" + state.view); }); } var chips = document.querySelectorAll("#dirFilters .kt-ai-filter-chip"); for (s = 0; s < chips.length; s++) { chips[s].addEventListener("click", function () { state.dir = this.getAttribute("data-dir") || ""; lsSet(LS_DIR, state.dir); renderDirChips(); renderAll(); }); } document.getElementById("dealSearch").addEventListener("input", function () { state.search = this.value; renderDeals(); }); syncPeriodSeg(); } function syncPeriodSeg() { var per = PERIOD_BY_ID[state.period]; var kind = per.period.toLowerCase() === "год" ? "year" : (per.period === "Квартал" ? "quarter" : "month"); var btns = document.querySelectorAll("#periodSeg .seg"); for (var i = 0; i < btns.length; i++) { if (btns[i].getAttribute("data-period") === kind) btns[i].classList.add("is-active"); else btns[i].classList.remove("is-active"); } } /* ---------- Старт ---------- */ function start() { document.getElementById("todayDate").textContent = "Данные на 25.09.2026"; renderDirChips(); bind(); var hash = location.hash.replace("#", ""); setView(hash === "detail" ? "detail" : state.view); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start); else start(); })();