diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..de745e1
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(node -e ':*)"
+ ]
+ }
+}
diff --git a/public/js/statistics.js b/public/js/statistics.js
index 0d0be32..7b74137 100644
--- a/public/js/statistics.js
+++ b/public/js/statistics.js
@@ -497,6 +497,12 @@ function renderStatsPage() {
+
+
+
👥 Заявки по клиентам (все, включая с нулём)
+
+
+
@@ -655,6 +661,7 @@ function renderStatsPage() {
renderClientsList();
renderClientLastTicketCard();
+ renderClientUsersCard();
saveStatsCache();
}
@@ -712,7 +719,14 @@ function renderClientsList() {
if(visible.length===0){wrap.innerHTML=`${clientsOnlyOpen?'Нет клиентов с открытыми заявками':'Нет повторных обращений'}
`;return;}
wrap.innerHTML=`
- ${visible.map(([name,v])=>{ const dv=clientsOnlyOpen?v.open:v.count,pct=dv/maxV*100,opPct=v.count?v.open/v.count*100:0; return'
'+escHtml(name)+'
'+v.count+''+(v.open?''+v.open+' откр.':'')+'
'; }).join('')}
+ ${visible.map(([name,v])=>{
+ const dv=clientsOnlyOpen?v.open:v.count,pct=dv/maxV*100,opPct=v.count?v.open/v.count*100:0;
+ const safeName=escHtml(name).replace(/'/g,''');
+ const openBadge=v.open
+ ?`
${v.open} откр.`
+ :`
`;
+ return`
${escHtml(name)}
${v.count}${openBadge}
`;
+ }).join('')}
${!clientsExpanded&&hidden>0?`+ ещё ${hidden} — показать все
`:''}`;
}
@@ -820,3 +834,50 @@ function renderClientLastTicketCard() {
return `${escHtml(name)}
${fmtAgo(age)}
${dateStr}
`;
}).join('')}`;
}
+
+function renderClientUsersCard() {
+ const wrap = document.getElementById('stats-client-users-wrap');
+ if (!wrap) return;
+
+ // Собираем: клиент → { total, users: { username → count } }
+ const cMap = {};
+ // Сначала добавляем всех клиентов, которые есть в allData, с нулями
+ allData.forEach(({task: t}) => {
+ const name = t._clientname || t.clientname || 'Неизвестно';
+ if (!cMap[name]) cMap[name] = { total: 0, users: {} };
+ });
+ allData.forEach(({task: t}) => {
+ const name = t._clientname || t.clientname || 'Неизвестно';
+ cMap[name].total++;
+ const user = t.username || '—';
+ cMap[name].users[user] = (cMap[name].users[user] || 0) + 1;
+ });
+
+ const entries = Object.entries(cMap).sort((a, b) => b[1].total - a[1].total);
+ if (!entries.length) { wrap.innerHTML = 'Нет данных
'; return; }
+ const maxTotal = entries[0][1].total || 1;
+
+ wrap.innerHTML = `${entries.map(([name, v]) => {
+ const pct = v.total / maxTotal * 100;
+ const safeName = escHtml(name).replace(/'/g, ''');
+ const usersList = Object.entries(v.users).sort((a, b) => b[1] - a[1]).map(([uname, cnt]) => {
+ const uPct = v.total ? cnt / v.total * 100 : 0;
+ return `
+
+
${escHtml(uname)}
+
${cnt}
+
${Math.round(uPct)}%
+
`;
+ }).join('');
+ return `
+
+
${escHtml(name)}
+
${v.total}
+
+
+ ${v.total > 0 ? usersList : ''}
+
`;
+ }).join('')}
`;
+}