From 621c6407fe2b3a6ed605c52c8b0ce50be9b165a0 Mon Sep 17 00:00:00 2001 From: dv Date: Wed, 15 Apr 2026 10:13:45 +0300 Subject: [PATCH] =?UTF-8?q?+=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 7 +++++ public/js/statistics.js | 63 ++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.local.json 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('')}
`; +}