(function () { "use strict"; /* ================= Демо-данные: отдел, 11 человек ================= */ var PERIODS = { "7d": { label: "период: 7 дней", metricKey: "tasks", metricName: "Задач закрыто", metricUnit: "зад.", kpis: [ { label: "Закрыто задач", value: "128", hint: "из 141 по плану", tone: "ok", delta: "+6%" }, { label: "Эффективность", value: "81%", hint: "цель 80%", tone: "ok", delta: "+2 п.п." }, { label: "Средняя оценка", value: "4,4", hint: "из 5,0" }, { label: "Переработка в среднем", value: "1,8 ч", hint: "на день", tone: "risk", delta: "+0,4 ч" } ], trend: [ { c: "Вт", a: 70, b: 66, s: 78 }, { c: "Ср", a: 74, b: 69, s: 81 }, { c: "Чт", a: 68, b: 72, s: 79 }, { c: "Пт", a: 71, b: 70, s: 82 }, { c: "Сб", a: 65, b: 63, s: 77 }, { c: "Вс", a: 62, b: 66, s: 76 }, { c: "Пн", a: 73, b: 74, s: 84 } ], services: [ { l: "Поддержка", v: 88, tone: "green" }, { l: "Аналитика", v: 79 }, { l: "Монтаж", v: 71 }, { l: "Коммерция", v: 64 } ], top: [ { n: "Айгерим Нурлан", d: "Поддержка", v: 93 }, { n: "Тимур Серик", d: "Монтаж", v: 88 }, { n: "Ержан Касым", d: "Поддержка", v: 84 }, { n: "Динар Жума", d: "Коммерция", v: 81 }, { n: "Мария Ильина", d: "Аналитика", v: 79 } ], table: [ { name: "Айгерим Нурлан", dept: "Поддержка", metric: 24, mins: 34, plan: 96, status: "ok", sLabel: "Выше плана" }, { name: "Ержан Касым", dept: "Поддержка", metric: 16, mins: 29, plan: 94, status: "ok", sLabel: "Выше плана" }, { name: "Тимур Серик", dept: "Монтаж", metric: 18, mins: 51, plan: 90, status: "ok", sLabel: "Выше плана" }, { name: "Мария Ильина", dept: "Аналитика", metric: 21, mins: 47, plan: 87, status: "ok", sLabel: "В плане" }, { name: "Динар Жума", dept: "Коммерция", metric: 12, mins: 44, plan: 83, status: "ok", sLabel: "В плане" }, { name: "Салтанат Аман", dept: "Аналитика", metric: 19, mins: 58, plan: 80, status: "warn", sLabel: "Рядом с планом" }, { name: "Нурлан Баек", dept: "Монтаж", metric: 14, mins: 56, plan: 77, status: "warn", sLabel: "Рядом с планом" }, { name: "Жанар Сапар", dept: "Поддержка", metric: 13, mins: 38, plan: 74, status: "warn", sLabel: "Рядом с планом" }, { name: "Берик Тлеу", dept: "Коммерция", metric: 11, mins: 61, plan: 71, status: "warn", sLabel: "Ниже плана" }, { name: "Айдын Тлеу", dept: "Монтаж", metric: 10, mins: 72, plan: 66, status: "risk", sLabel: "Ниже плана" }, { name: "Алия Мырзахм", dept: "Аналитика", metric: 9, mins: 66, plan: 62, status: "risk", sLabel: "Ниже плана" } ], note: "К концу недели показатели выше начала — пик загрузки выпал на последние дни.", serviceNote: "Поддержка стабильно выше остальных", conclusion: "За неделю закрыто 128 задач при плане 141. Лучший показатель — у Айгерим Нурлан (24 задачи, 96% плана). Двое сотрудников ниже плана: Айдын Тлеу и Алия Мырзахм — стоит сверить распределение задач." }, "30d": { label: "период: 30 дней", metricKey: "tasks", metricName: "Задач закрыто", metricUnit: "зад.", kpis: [ { label: "Закрыто задач", value: "512", hint: "из 540 по плану", tone: "warn", delta: "−5%" }, { label: "Эффективность", value: "76%", hint: "цель 80%", tone: "warn", delta: "−1,4 п.п." }, { label: "Средняя оценка", value: "4,2", hint: "из 5,0" }, { label: "Переработка в среднем", value: "2,1 ч", hint: "на день", tone: "risk", delta: "+0,3 ч" } ], trend: [ { c: "Нед. 1", a: 118, b: 124, s: 121 }, { c: "Нед. 2", a: 126, b: 121, s: 124 }, { c: "Нед. 3", a: 132, b: 138, s: 135 }, { c: "Нед. 4", a: 108, b: 120, s: 102 } ], services: [ { l: "Поддержка", v: 82 }, { l: "Аналитика", v: 74 }, { l: "Монтаж", v: 68, tone: "orange" }, { l: "Коммерция", v: 61, tone: "orange" } ], top: [ { n: "Айгерим Нурлан", d: "Поддержка", v: 92 }, { n: "Мария Ильина", d: "Аналитика", v: 90 }, { n: "Тимур Серик", d: "Монтаж", v: 85 }, { n: "Ержан Касым", d: "Поддержка", v: 82 }, { n: "Салтанат Аман", d: "Аналитика", v: 80 } ], table: [ { name: "Айгерим Нурлан", dept: "Поддержка", metric: 96, mins: 35, plan: 95, status: "ok", sLabel: "Выше плана" }, { name: "Мария Ильина", dept: "Аналитика", metric: 104, mins: 48, plan: 92, status: "ok", sLabel: "Выше плана" }, { name: "Ержан Касым", dept: "Поддержка", metric: 88, mins: 31, plan: 90, status: "ok", sLabel: "В плане" }, { name: "Тимур Серик", dept: "Монтаж", metric: 86, mins: 52, plan: 88, status: "ok", sLabel: "В плане" }, { name: "Салтанат Аман", dept: "Аналитика", metric: 92, mins: 60, plan: 84, status: "ok", sLabel: "В плане" }, { name: "Динар Жума", dept: "Коммерция", metric: 74, mins: 45, plan: 79, status: "warn", sLabel: "Рядом с планом" }, { name: "Жанар Сапар", dept: "Поддержка", metric: 84, mins: 39, plan: 76, status: "warn", sLabel: "Рядом с планом" }, { name: "Нурлан Баек", dept: "Монтаж", metric: 70, mins: 58, plan: 73, status: "warn", sLabel: "Ниже плана" }, { name: "Берик Тлеу", dept: "Коммерция", metric: 62, mins: 63, plan: 68, status: "warn", sLabel: "Ниже плана" }, { name: "Айдын Тлеу", dept: "Монтаж", metric: 55, mins: 74, plan: 62, status: "risk", sLabel: "Ниже плана" }, { name: "Алия Мырзахм", dept: "Аналитика", metric: 58, mins: 67, plan: 58, status: "risk", sLabel: "Ниже плана" } ], note: "Четвёртая неделя — заметно ниже первых трёх: план не выполнен на 17%.", serviceNote: "У монтажа и коммерции — наихудшая динамика", conclusion: "За месяц закрыто 512 задач из плана 540. Четвёртая неделя дала на 17% меньше, чем средняя по первым трём — к концу месяца план не закрыт. Эффективность 76% при цели 80%; отстают монтаж и коммерция." }, "quarter": { label: "период: квартал", metricKey: "tasks", metricName: "Задач закрыто", metricUnit: "зад.", kpis: [ { label: "Закрыто задач", value: "1 486", hint: "из 1 520 по плану", tone: "warn", delta: "−2%" }, { label: "Эффективность", value: "79%", hint: "цель 80%", tone: "warn", delta: "−0,6 п.п." }, { label: "Средняя оценка", value: "4,1", hint: "из 5,0" }, { label: "Переработка в среднем", value: "2,3 ч", hint: "на день", tone: "risk", delta: "+0,5 ч" } ], trend: [ { c: "Июнь", a: 448, b: 470, s: 459 }, { c: "Июль", a: 462, b: 488, s: 475 }, { c: "Август", a: 489, b: 511, s: 500 } ], services: [ { l: "Поддержка", v: 84, tone: "green" }, { l: "Аналитика", v: 77 }, { l: "Монтаж", v: 70 }, { l: "Коммерция", v: 66 } ], top: [ { n: "Айгерим Нурлан", d: "Поддержка", v: 94 }, { name: "Мария Ильина", d: "Аналитика", v: 89 }, { n: "Тимур Серик", d: "Монтаж", v: 86 }, { n: "Салтанат Аман", d: "Аналитика", v: 83 }, { n: "Ержан Касым", d: "Поддержка", v: 81 } ], table: [ { name: "Айгерим Нурлан", dept: "Поддержка", metric: 268, mins: 36, plan: 94, status: "ok", sLabel: "Выше плана" }, { name: "Мария Ильина", dept: "Аналитика", metric: 294, mins: 49, plan: 89, status: "ok", sLabel: "Выше плана" }, { name: "Тимур Серик", dept: "Монтаж", metric: 252, mins: 53, plan: 86, status: "ok", sLabel: "В плане" }, { name: "Ержан Касым", dept: "Поддержка", metric: 246, mins: 32, plan: 85, status: "ok", sLabel: "В плане" }, { name: "Салтанат Аман", dept: "Аналитика", metric: 271, mins: 61, plan: 83, status: "ok", sLabel: "В плане" }, { name: "Динар Жума", dept: "Коммерция", metric: 208, mins: 46, plan: 79, status: "ok", sLabel: "В плане" }, { name: "Жанар Сапар", dept: "Поддержка", metric: 239, mins: 40, plan: 76, status: "warn", sLabel: "Рядом с планом" }, { name: "Нурлан Баек", dept: "Монтаж", metric: 217, mins: 59, plan: 73, status: "warn", sLabel: "Рядом с планом" }, { name: "Берик Тлеу", dept: "Коммерция", metric: 198, mins: 64, plan: 68, status: "warn", sLabel: "Ниже плана" }, { name: "Алидин Тлеу", dept: "Монтаж", metric: 185, mins: 76, plan: 61, status: "risk", sLabel: "Ниже плана" }, { name: "Алия Мырзахм", dept: "Аналитика", metric: 195, mins: 68, plan: 57, status: "risk", sLabel: "Ниже плана" } ], note: "Тренд положительный: за квартал — прирост 10,6% закрытых задач к июню.", serviceNote: "Поддержка держит план по всем месяцам", conclusion: "За квартал — 1 486 задач из плана 1 520. Общий тренд положительный (+10,6% к июню), но план за месяц не закрыт. Эффективность 79% при цели 80%, переработка выросла до 2,3 ч в день — нагрузка выше проектной." } }; /* ================= Состояние и localStorage ================= */ var LS_KEY = "dash-effect-v1"; function defaultState() { return { period: "30d", search: "", dept: "all", sortKey: "metric", sortDir: "desc" }; } function loadState() { try { var raw = localStorage.getItem(LS_KEY); if (!raw) return defaultState(); var parsed = JSON.parse(raw); var st = defaultState(); if (parsed && typeof parsed === "object") { if (PERIODS[parsed.period]) st.period = parsed.period; if (typeof parsed.search === "string") st.search = parsed.search; if (typeof parsed.dept === "string") st.dept = parsed.dept; if (parsed.sortKey === "metric" || parsed.sortKey === "time") st.sortKey = parsed.sortKey; if (parsed.sortDir === "asc" || parsed.sortDir === "desc") st.sortDir = parsed.sortDir; } return st; } catch (e) { return defaultState(); } } function saveState() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {} } var state = loadState(); /* ================= Утилиты ================= */ function el(id) { return document.getElementById(id); } function esc(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function fmt(mins) { var h = Math.floor(mins / 60); var m = Math.round(mins % 60); if (!h) return m + " мин"; return h + " ч " + m + " мин"; } function todayStr() { var d = new Date(); var p = function (n) { return String(n).length < 2 ? "0" + String(n) : String(n); }; return p(d.getDate()) + "." + p(d.getMonth() + 1) + "." + d.getFullYear(); } function toast(msg, tone) { var stack = document.querySelector(".kt-ai-toast-stack"); if (!stack) return; var t = document.createElement("div"); t.className = "kt-ai-toast"; if (tone) t.setAttribute("data-tone", tone); t.textContent = msg; stack.appendChild(t); setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3000); } function deltaHtml(item) { if (!item.delta) return ""; var tone = item.tone === "risk" ? "risk" : "ok"; return '' + esc(item.delta) + " к прошлому периоду"; } /* ================= Данные периода ================= */ function data(period) { var d = PERIODS[period] || PERIODS["30d"]; var rows = (d.table || []).map(function (r) { return { name: r.name, dept: r.dept, metric: Number(r.metric) || 0, mins: Number(r.mins) || 0, plan: Number(r.plan) || 0, status: r.status, sLabel: r.sLabel }; }); /* статус строится из плана — согласуется даже при расхождениях данных */ rows = rows.map(function (r) { var s = r.plan >= 88 ? "ok" : r.plan >= 75 ? "warn" : "risk"; var l = r.plan >= 88 ? "Выше плана" : r.plan >= 75 ? "Рядом с планом" : "Ниже плана"; return { name: r.name, dept: r.dept, metric: r.metric, mins: r.mins, plan: r.plan, status: s, sLabel: l }; }); var depts = []; var seen = {}; rows.forEach(function (r) { if (!seen[r.dept]) { seen[r.dept] = true; depts.push(r.dept); } }); return { raw: d, rows: rows, depts: depts }; } /* ================= KPI ================= */ function renderKpis(period) { var d = data(period); var wrap = el("kpi-strip"); if (!wrap) return; wrap.innerHTML = d.raw.kpis.map(function (k) { return '
' + '' + esc(k.label) + "" + '' + esc(k.value) + deltaHtml(k) + "" + '' + esc(k.hint) + "" + "
"; }).join(""); } /* ================= Линейный график ================= */ var LW = 720, LH = 208, LR = { t: 12, r: 14, b: 14, l: 10 }; function lineData(period) { var t = data(period).raw.trend || []; return t.map(function (p) { return { c: p.c, a: Number(p.a) || 0, b: Number(p.b) || 0, s: Number(p.s) || 0 }; }); } function renderLine(period) { var pts = lineData(period); var grid = el("line-grid"); var area = el("line-area"); var path = el("line-path"); var gPts = el("line-points"); if (!pts.length || !grid || !area || !path || !gPts) return; var w = LW - LR.l - LR.r; var h = LH - LR.t - LR.b; var vals = [10]; pts.forEach(function (p) { vals.push(p.a, p.b, p.s); }); var maxV = Math.max.apply(null, vals); var y = function (v) { return LR.t + (1 - v / maxV) * h; }; var x = function (i) { return LR.l + (pts.length === 1 ? w / 2 : (i / (pts.length - 1)) * w); }; var gridHtml = ""; for (var g = 0; g <= 3; g++) { var gy = LR.t + (g / 3) * h; gridHtml += ''; } grid.innerHTML = gridHtml; function linePath(key) { var s = ""; pts.forEach(function (p, i) { s += (i === 0 ? "M" : "L") + x(i).toFixed(1) + " " + y(p[key]).toFixed(1) + " "; }); return s; } path.setAttribute("d", linePath("s")); area.setAttribute("points", (LR.l) + "," + (LH - LR.b) + " " + pts.map(function (p, i) { return x(i).toFixed(1) + "," + y(p.s).toFixed(1); }).join(" ") + " " + (LW - LR.r) + "," + (LH - LR.b) ); var ptHtml = ""; pts.forEach(function (p, i) { ptHtml += ''; }); gPts.innerHTML = ptHtml; var axis = el("line-axis"); if (axis) { axis.setAttribute("data-line", "true"); axis.innerHTML = pts.map(function (p) { return "" + esc(p.c) + ""; }).join(""); } var valEl = el("trend-value"); if (valEl) { var last = pts[pts.length - 1]; valEl.textContent = String(last ? last.s : 0); } var note = el("trend-note"); if (note) note.textContent = data(period).raw.note || ""; } /* ================= Столбчатый график: направления ================= */ function renderServices(period) { var raw = data(period).raw; var bars = el("service-bars"); if (!bars) return; var svcs = raw.services || []; var max = 0; svcs.forEach(function (s) { if (s.v > max) max = s.v; }); if (!max) max = 1; var toneMap = { green: "var(--kt-ai-chart-green)", orange: "var(--kt-ai-chart-orange)", red: "var(--kt-ai-chart-red)" }; bars.innerHTML = svcs.map(function (s) { var h = Math.max(4, Math.round((s.v / max) * 120)); var color = toneMap[s.tone] || "var(--kt-ai-chart-blue)"; return '
' + "
"; }).join(""); var axis = bars.parentNode.querySelector(".cc-axis"); if (axis) { axis.classList.add("cc-axis-cat"); axis.innerHTML = svcs.map(function (s) { return "" + esc(s.l) + ""; }).join(""); } var note = el("service-note"); if (note) note.textContent = raw.serviceNote || ""; } /* ================= Топ сотрудников ================= */ function renderTop(period) { var wrap = el("top-list"); if (!wrap) return; var top = data(period).raw.top || []; var max = 0; top.forEach(function (t) { if ((t.v || t) > max) max = (t.v || t); }); wrap.innerHTML = top.map(function (t, i) { var name = t.name || t.n || "—"; var v = t.v || t; var dp = t.d || ""; var w = max ? Math.round((v / max) * 100) : 0; return '
' + '" + String(i + 1) + "" + '' + esc(name) + (dp ? '' + esc(dp) + "" : "") + "" + '' + '' + String(v) + " п." + "
"; }).join(""); } /* ================= Таблица ================= */ function visibleRows() { var d = data(state.period); var q = state.search.trim().toLowerCase(); var rows = d.rows.filter(function (r) { if (state.dept !== "all" && r.dept !== state.dept) return false; if (q && r.name.toLowerCase().indexOf(q) === -1) return false; return true; }); var key = state.sortKey === "time" ? "mins" : "metric"; var dir = state.sortDir === "asc" ? 1 : -1; rows.sort(function (a, b) { if (a[key] === b[key]) return b.metric - a.metric; return (a[key] - b[key]) * dir; }); return rows; } function renderTable() { var body = el("table-body"); var empty = el("empty"); var chip = el("count-chip"); var hint = el("filter-hint"); var table = el("summary-table"); var rows = visibleRows(); if (!body) return; var d = data(state.period); if (chip) chip.textContent = rows.length + " из " + d.rows.length; if (hint) { if (state.dept !== "all" || state.search.trim()) { hint.textContent = "применены фильтры"; } else { hint.textContent = ""; } } body.innerHTML = rows.map(function (r, i) { var top = i < 3 && !state.search.trim() && state.dept === "all" && state.sortKey === "metric" && state.sortDir === "desc"; var p = Math.max(0, Math.min(100, r.plan)); return '" + "" + String(i + 1) + "" + esc(r.name) + "" + "" + esc(r.dept) + "" + '' + String(r.metric) + " " + esc(d.raw.metricUnit) + "" + '' + fmt(r.mins) + "" + '
' + '' + String(r.plan) + "%
" + '' + esc(r.sLabel) + "" + ""; }).join(""); if (table) table.style.display = rows.length ? "" : "none"; if (empty) empty.hidden = rows.length > 0; } /* ================= Общие блоки ================= */ function renderAll() { var d = data(state.period); if (!d) return; var chip = el("period-chip"); if (chip) chip.textContent = d.raw.label; var meta = el("dash-meta"); if (meta) { var total = 0; d.rows.forEach(function (r) { total += r.metric; }); meta.textContent = "обновлено " + todayStr() + " · " + d.rows.length + " сотрудников · " + total + " " + d.raw.metricUnit + " за период"; } var name = el("th-metric-name"); if (name) name.textContent = d.raw.metricName; var thM = el("th-metric"); var thT = el("th-time"); if (thM) thM.setAttribute("aria-sort", state.sortKey === "metric" ? (state.sortDir === "asc" ? "ascending" : "descending") : "none"); if (thT) thT.setAttribute("aria-sort", state.sortKey === "time" ? (state.sortDir === "asc" ? "ascending" : "descending") : "none"); var icM = thM ? thM.querySelector(".th-sort-ic") : null; var icT = thT ? thT.querySelector(".th-sort-ic") : null; if (icM) icM.setAttribute("data-dir", state.sortKey === "metric" ? state.sortDir : "desc"); if (icT) icT.setAttribute("data-dir", state.sortKey === "time" ? state.sortDir : "desc"); var sel = el("filter-dept"); if (sel) { var opts = ''; d.depts.forEach(function (dp) { opts += '"; }); sel.innerHTML = opts; } var search = el("search"); if (search && search.value !== state.search) search.value = state.search; var tabs = el("period-tabs"); if (tabs) { var segs = tabs.querySelectorAll(".seg"); for (var i = 0; i < segs.length; i++) { segs[i].setAttribute("data-active", segs[i].getAttribute("data-period") === state.period ? "true" : "false"); } } renderKpis(state.period); renderLine(state.period); renderServices(state.period); renderTop(state.period); renderTable(); var conc = document.querySelector("#conclusion .text"); if (conc) conc.textContent = d.raw.conclusion || ""; var cwrap = el("conclusion"); if (cwrap) cwrap.hidden = !d.raw.conclusion; var upd = el("footer-updated"); if (upd) upd.textContent = "данные на " + todayStr(); saveState(); } /* ================= События ================= */ function setPeriod(p) { if (!PERIODS[p]) return; state.period = p; renderAll(); } function setSort(key) { if (state.sortKey === key) { state.sortDir = state.sortDir === "asc" ? "desc" : "asc"; } else { state.sortKey = key; state.sortDir = "desc"; } renderTable(); var thM = el("th-metric"); var thT = el("th-time"); if (thM) thM.setAttribute("aria-sort", state.sortKey === "metric" ? (state.sortDir === "asc" ? "ascending" : "descending") : "none"); if (thT) thT.setAttribute("aria-sort", state.sortKey === "time" ? (state.sortDir === "asc" ? "ascending" : "descending") : "none"); saveState(); } function bindEvents() { var tabs = el("period-tabs"); if (tabs) { tabs.addEventListener("click", function (ev) { var btn = ev.target && ev.target.closest ? ev.target.closest(".seg") : null; if (btn) setPeriod(btn.getAttribute("data-period")); }); } var bRefresh = el("btn-refresh"); if (bRefresh) { bRefresh.addEventListener("click", function () { var p = state.period; state = defaultState(); state.period = p; renderAll(); toast("Данные обновлены, фильтры сброшены", "ok"); }); } var bMetric = el("th-sort-metric"); if (bMetric) bMetric.addEventListener("click", function () { setSort("metric"); }); var bTime = el("th-sort-time"); if (bTime) bTime.addEventListener("click", function () { setSort("time"); }); var search = el("search"); if (search) { search.addEventListener("input", function () { state.search = search.value; renderTable(); saveState(); }); } var sel = el("filter-dept"); if (sel) { sel.addEventListener("change", function () { state.dept = sel.value; renderTable(); saveState(); }); } var bEmpty = el("btn-empty-reset"); if (bEmpty) { bEmpty.addEventListener("click", function () { state.search = ""; state.dept = "all"; renderAll(); }); } } /* ================= Старт ================= */ renderAll(); bindEvents(); if (typeof window.initKtChartTip === "function") { try { window.initKtChartTip(); } catch (e) {} } })();