597 lines
26 KiB
JavaScript
597 lines
26 KiB
JavaScript
(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, ">").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 '<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) {}
|
||
}
|
||
})();
|