dashbord-po-prodazham-b2b/script.js

558 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Дашборд по продажам 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 += '<div class="kt-ai-kpi">';
html += '<span class="label">' + kpis[i].label + "</span>";
html += '<span class="value"' + (kpis[i].value.length > 10 ? ' data-long="true"' : "") + ">" + kpis[i].value + "</span>";
html += '<span class="hint">' + kpis[i].hint + "</span>";
if (kpis[i].delta) html += '<span class="delta ' + (kpis[i].deltaVal >= 0 ? "ok" : "down") + '">' + kpis[i].delta + "</span>";
html += "</div>";
}
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 += '<div class="cc-bar" tabindex="0" data-kt-tip="' + slots[s].label + ": " + fmtMoneyShort(slots[s].value) + '"><i style="height:' + Math.max(h, 1) + '%"></i></div>';
axis += "<span>" + slots[s].label + "</span>";
}
var per = PERIOD_BY_ID[state.period];
document.getElementById("trendChart").innerHTML =
'<div class="cc-head"><div>' +
'<div class="cc-title">Динамика выручки по месяцам</div>' +
'<div class="cc-total"><span class="num">' + fmtMoneyShort(total) + "</span><span class="unit">закрытые сделки</span></div></div>" +
'<span class="kt-ai-chip" data-tone="blue">' + per.label + "</span></div>" +
'<div class="cc-chart">' + bars + "</div>" +
'<div class="cc-axis">' + axis + "</div>";
}
/* ---------- По направлениям ---------- */
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 +=
'<div class="cat-row">' +
'<span class="cat-name">' + cat + "</span>" +
'<span class="cat-val">' + fmtMoneyShort(totals[cat]) + "</span>" +
'<span class="cat-track"><i style="width:' + Math.max(w, 1) + "%;background:" + catColor(cat) + '"></i></span></div>';
}
document.getElementById("categoryChart").innerHTML =
'<div class="cc-title" style="margin-bottom:var(--kt-ai-space-4)">Выручка по направлениям</div>' +
'<div class="cat-list">' + rows + "</div>";
}
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 +=
'<div class="top-row">' +
'<span class="top-rank">' + (i + 1) + "</span>" +
'<div class="top-info"><div class="top-name">' + arr[i].name + '</div><div class="top-meta">' + arr[i].dir + "</div></div>" +
'<span class="top-sum">' + fmtMoneyShort(arr[i].sum) + "</span></div>";
}
document.getElementById("topClients").innerHTML =
'<div class="card-cap">Крупнейшие клиенты периода</div>' +
'<div class="toplist">' + rows + "</div>";
}
/* ---------- Детализация: фильтры и таблица ---------- */
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 =
'<button class="kt-ai-filter-chip" data-dir="" type="button' +
(state.dir === "" ? '" data-active="true' : "") + ">Все направления</button>";
for (var i = 0; i < DIRS.length; i++) {
html +=
'<button class="kt-ai-filter-chip" data-dir="' + DIRS[i].id + '" type="button' +
(state.dir === DIRS[i].id ? '" data-active="true' : "") + ">" + DIRS[i].name + "</button>";
}
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 +=
"<tr>" +
'<td class="kt-ai-deal-c"><strong>' + rows[i].client + '</strong><span class="deal-dir">' + rows[i].dir + "</span></td>" +
"<td>" + rows[i].service + '<span class="deal-dir">' + rows[i].category + "</span></td>" +
"<td>" + rows[i].manager + "</td>" +
'<td class="num">' + fmtMoney(rows[i].amount) + "</td>" +
'<td><span class="kt-ai-status-pill" data-status="' + st.status + '">' + st.label + "</span></td>" +
"</tr>";
}
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();
})();