фикс статистики 11
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
dv 2026-04-15 13:52:55 +03:00
parent 57fd5a4839
commit f2d3b5fc96

View file

@ -839,6 +839,17 @@ function renderClientUsersCard() {
const wrap = document.getElementById('stats-client-users-wrap'); const wrap = document.getElementById('stats-client-users-wrap');
if (!wrap) return; if (!wrap) return;
const UNKNOWN_USER_KEY = '__unknown__';
const normalizeUserName = (value) => String(value || '')
.replace(/\u00A0/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
const displayUserName = (value) => String(value || '')
.replace(/\u00A0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
// createdby — строка с именем создателя, приходит прямо из API // createdby — строка с именем создателя, приходит прямо из API
// Базовый список клиентов берём из _allClients (все usergroup из dictionaries), // Базовый список клиентов берём из _allClients (все usergroup из dictionaries),
// чтобы показывать клиентов с нулём заявок тоже. // чтобы показывать клиентов с нулём заявок тоже.
@ -848,18 +859,30 @@ function renderClientUsersCard() {
// Сначала заполняем всех известных клиентов с нулями, включая всех пользователей группы // Сначала заполняем всех известных клиентов с нулями, включая всех пользователей группы
(window._allClients || []).forEach((c) => { (window._allClients || []).forEach((c) => {
const users = {}; const users = {};
(clientUsers[String(c.client_id)] || []).forEach((u) => { users[u] = 0; }); const userLabels = {};
cMap[c.name] = { total: 0, users }; (clientUsers[String(c.client_id)] || []).forEach((u) => {
const display = displayUserName(u);
const key = normalizeUserName(display);
if (!key) return;
users[key] = 0;
if (!userLabels[key]) userLabels[key] = display;
});
cMap[c.name] = { total: 0, users, userLabels };
}); });
// Затем считаем реальные заявки // Затем считаем реальные заявки
allData.forEach((item) => { allData.forEach((item) => {
const t = item.task; const t = item.task;
const org = t._clientname || t.clientname || 'Неизвестно'; const org = t._clientname || t.clientname || 'Неизвестно';
const creator = t.createdby || '—'; const creatorDisplay = displayUserName(t.createdby);
if (!cMap[org]) cMap[org] = { total: 0, users: {} }; const isUnknownCreator = !creatorDisplay || creatorDisplay === '\u2014' || creatorDisplay === '-';
const creatorKey = isUnknownCreator ? UNKNOWN_USER_KEY : normalizeUserName(creatorDisplay);
if (!cMap[org]) cMap[org] = { total: 0, users: {}, userLabels: {} };
cMap[org].total++; cMap[org].total++;
cMap[org].users[creator] = (cMap[org].users[creator] || 0) + 1; cMap[org].users[creatorKey] = (cMap[org].users[creatorKey] || 0) + 1;
if (!isUnknownCreator && !cMap[org].userLabels[creatorKey]) {
cMap[org].userLabels[creatorKey] = creatorDisplay;
}
}); });
const entries = Object.entries(cMap).sort((a, b) => b[1].total - a[1].total); const entries = Object.entries(cMap).sort((a, b) => b[1].total - a[1].total);
@ -870,9 +893,10 @@ function renderClientUsersCard() {
const pct = v.total / maxTotal * 100; const pct = v.total / maxTotal * 100;
const safeName = escHtml(name).replace(/'/g, '''); const safeName = escHtml(name).replace(/'/g, ''');
const sortedUsers = Object.entries(v.users).sort((a, b) => b[1] - a[1]); const sortedUsers = Object.entries(v.users).sort((a, b) => b[1] - a[1]);
const knownUsers = sortedUsers.filter(([u]) => u !== '—'); const knownUsers = sortedUsers.filter(([key]) => key !== UNKNOWN_USER_KEY);
const unknownCount = v.users['—'] || 0; const unknownCount = v.users[UNKNOWN_USER_KEY] || 0;
const usersList = knownUsers.map(([uname, cnt]) => { const usersList = knownUsers.map(([key, cnt]) => {
const uname = v.userLabels?.[key] || key;
const uPct = v.total ? cnt / v.total * 100 : 0; const uPct = v.total ? cnt / v.total * 100 : 0;
return `<div style="display:flex;align-items:center;gap:6px;margin-top:3px;padding-left:4px;"> return `<div style="display:flex;align-items:center;gap:6px;margin-top:3px;padding-left:4px;">
<div style="width:10px;height:10px;border-radius:50%;background:${avatarColor(uname)};flex-shrink:0;"></div> <div style="width:10px;height:10px;border-radius:50%;background:${avatarColor(uname)};flex-shrink:0;"></div>