dashbord/script.js

597 lines
26 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.

(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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 '<span class="delta" data-tone="' + tone + '">' + esc(item.delta) + " к прошлому периоду</span>";
}
/* ================= Данные периода ================= */
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 '<div class="kt-ai-kpi" role="listitem">' +
'<span class="label">' + esc(k.label) + "</span>" +
'<span class="value">' + esc(k.value) + deltaHtml(k) + "</span>" +
'<span class="hint">' + esc(k.hint) + "</span>" +
"</div>";
}).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 += '<line x1="' + LR.l + '" y1="' + gy + '" x2="' + (LW - LR.r) + '" y2="' + gy + '"></line>';
}
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 += '<circle class="pt" cx="' + x(i).toFixed(1) + '" cy="' + y(p.s).toFixed(1) + '" r="4" ' +
'data-kt-tip=\'{"title":"' + esc(p.c) + '","rows":[["Показатель","' + String(p.s) + ' п."]]}\'></circle>';
});
gPts.innerHTML = ptHtml;
var axis = el("line-axis");
if (axis) {
axis.setAttribute("data-line", "true");
axis.innerHTML = pts.map(function (p) { return "<span>" + esc(p.c) + "</span>"; }).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 '<div class="cc-bar" style="--bar-w:24px" data-kt-tip=\'{"title":"' + esc(s.l) + '","rows":[["Показатель","' + String(s.v) + ' п."]]}\'> ' +
"<i style=\"height:" + h + "px;background:" + color + "\"></i></div>";
}).join("");
var axis = bars.parentNode.querySelector(".cc-axis");
if (axis) {
axis.classList.add("cc-axis-cat");
axis.innerHTML = svcs.map(function (s) { return "<span>" + esc(s.l) + "</span>"; }).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 '<div class="top-row">' +
'<span class="rank" ' + (i < 3 ? 'data-top="true"' : "") + ">" + String(i + 1) + "</span>" +
'<span class="top-name">' + esc(name) + (dp ? '<span class="sub">' + esc(dp) + "</span>" : "") + "</span>" +
'<span class="top-bar"><i style="width:' + w + '%"></i></span>' +
'<span class="top-val">' + String(v) + " п.</span>" +
"</div>";
}).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 '<tr ' + (top ? 'data-top="true"' : "") + ">" +
"<td><span class=\"rank\">" + String(i + 1) + "</span>" + esc(r.name) + "</td>" +
"<td>" + esc(r.dept) + "</td>" +
'<td class="num">' + String(r.metric) + " " + esc(d.raw.metricUnit) + "</td>" +
'<td class="num">' + fmt(r.mins) + "</td>" +
'<td><div class="progress-cell"><div class="progress-line"><div class="progress-track"><i style="width:' + p + '%"></i></div>' +
'<span class="progress-num">' + String(r.plan) + "%</span></div></div></td>" +
'<td><span class="kt-ai-status-pill" data-status="' + r.status + '">' + esc(r.sLabel) + "</span></td>" +
"</tr>";
}).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 = '<option value="all">Все направления</option>';
d.depts.forEach(function (dp) {
opts += '<option value="' + esc(dp) + '" ' + (state.dept === dp ? 'selected="selected"' : "") + ">" + esc(dp) + "</option>";
});
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) {}
}
})();