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('')}
`; +}