+статистика клиентов
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
8cb0c58004
commit
621c6407fe
2 changed files with 69 additions and 1 deletions
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node -e ':*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -497,6 +497,12 @@ function renderStatsPage() {
|
||||||
<div id="stats-client-last-ticket-wrap"></div>
|
<div id="stats-client-last-ticket-wrap"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Заявки по клиентам + разбивка по пользователям -->
|
||||||
|
<div class="stats-card">
|
||||||
|
<div class="stats-card-title">👥 Заявки по клиентам (все, включая с нулём)</div>
|
||||||
|
<div id="stats-client-users-wrap"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
<!-- НОВЫЕ КАРТОЧКИ -->
|
<!-- НОВЫЕ КАРТОЧКИ -->
|
||||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||||
|
|
@ -655,6 +661,7 @@ function renderStatsPage() {
|
||||||
|
|
||||||
renderClientsList();
|
renderClientsList();
|
||||||
renderClientLastTicketCard();
|
renderClientLastTicketCard();
|
||||||
|
renderClientUsersCard();
|
||||||
saveStatsCache();
|
saveStatsCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -712,7 +719,14 @@ function renderClientsList() {
|
||||||
if(visible.length===0){wrap.innerHTML=`<div class="stats-empty">${clientsOnlyOpen?'Нет клиентов с открытыми заявками':'Нет повторных обращений'}</div>`;return;}
|
if(visible.length===0){wrap.innerHTML=`<div class="stats-empty">${clientsOnlyOpen?'Нет клиентов с открытыми заявками':'Нет повторных обращений'}</div>`;return;}
|
||||||
wrap.innerHTML=`
|
wrap.innerHTML=`
|
||||||
<div class="stats-clients-list" style="display:flex;flex-direction:column;gap:6px;">
|
<div class="stats-clients-list" style="display:flex;flex-direction:column;gap:6px;">
|
||||||
${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'<div class="stats-clients-row"><div class="stats-clients-name" title="'+escHtml(name)+'">'+escHtml(name)+'</div><div class="stats-clients-track"><div class="stats-clients-bar-total" style="width:'+pct+'%;"></div><div class="stats-clients-bar-open" style="width:'+pct*opPct/100+'%;"></div></div><div class="stats-clients-counts"><span class="stats-clients-total">'+v.count+'</span>'+(v.open?'<span class="stats-clients-open-badge">'+v.open+' откр.</span>':'')+'</div></div>'; }).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
|
||||||
|
?`<span class="stats-clients-open-badge" style="min-width:52px;text-align:center;">${v.open} откр.</span>`
|
||||||
|
:`<span style="min-width:52px;display:inline-block;"></span>`;
|
||||||
|
return`<div class="stats-clients-row" style="cursor:pointer;" onclick="goToClientTasks('${safeName}')" title="Показать заявки: ${safeName}"><div class="stats-clients-name" title="${safeName}">${escHtml(name)}</div><div class="stats-clients-track"><div class="stats-clients-bar-total" style="width:${pct}%;"></div><div class="stats-clients-bar-open" style="width:${pct*opPct/100}%;"></div></div><div class="stats-clients-counts" style="min-width:110px;justify-content:flex-end;"><span class="stats-clients-total" style="min-width:24px;text-align:right;">${v.count}</span>${openBadge}</div></div>`;
|
||||||
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
${!clientsExpanded&&hidden>0?`<div style="text-align:center;margin-top:10px;font-size:12px;color:var(--text3);">+ ещё ${hidden} — <span style="color:var(--accent);cursor:pointer;" onclick="clientsExpanded=true;document.getElementById('clients-expand-btn').textContent='▲ Свернуть';renderClientsList()">показать все</span></div>`:''}`;
|
${!clientsExpanded&&hidden>0?`<div style="text-align:center;margin-top:10px;font-size:12px;color:var(--text3);">+ ещё ${hidden} — <span style="color:var(--accent);cursor:pointer;" onclick="clientsExpanded=true;document.getElementById('clients-expand-btn').textContent='▲ Свернуть';renderClientsList()">показать все</span></div>`:''}`;
|
||||||
}
|
}
|
||||||
|
|
@ -820,3 +834,50 @@ function renderClientLastTicketCard() {
|
||||||
return `<div class="stats-bar-row" style="cursor:pointer;" onclick="goToClientTasks('${escHtml(name).replace(/'/g,"\\'")}')"><div class="stats-bar-label" title="${escHtml(name)}">${escHtml(name)}</div><div class="stats-bar-track"><div class="stats-bar-fill" style="width:${pct}%;background:${color};"></div></div><div class="stats-bar-count" style="min-width:90px;font-size:10px;color:var(--text2);">${fmtAgo(age)}</div><div class="stats-bar-pct" style="min-width:80px;font-size:10px;color:var(--text3);">${dateStr}</div></div>`;
|
return `<div class="stats-bar-row" style="cursor:pointer;" onclick="goToClientTasks('${escHtml(name).replace(/'/g,"\\'")}')"><div class="stats-bar-label" title="${escHtml(name)}">${escHtml(name)}</div><div class="stats-bar-track"><div class="stats-bar-fill" style="width:${pct}%;background:${color};"></div></div><div class="stats-bar-count" style="min-width:90px;font-size:10px;color:var(--text2);">${fmtAgo(age)}</div><div class="stats-bar-pct" style="min-width:80px;font-size:10px;color:var(--text3);">${dateStr}</div></div>`;
|
||||||
}).join('')}</div>`;
|
}).join('')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = '<div class="stats-empty">Нет данных</div>'; return; }
|
||||||
|
const maxTotal = entries[0][1].total || 1;
|
||||||
|
|
||||||
|
wrap.innerHTML = `<div style="display:flex;flex-direction:column;gap:10px;max-height:500px;overflow-y:auto;">${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 `<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>
|
||||||
|
<span style="font-size:11px;color:var(--text2);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${escHtml(uname)}">${escHtml(uname)}</span>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:var(--text);min-width:20px;text-align:right;">${cnt}</span>
|
||||||
|
<span style="font-size:10px;color:var(--text3);min-width:34px;text-align:right;">${Math.round(uPct)}%</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
return `<div style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;cursor:pointer;" onclick="goToClientTasks('${safeName}')">
|
||||||
|
<div style="font-size:12px;font-weight:600;color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${safeName}">${escHtml(name)}</div>
|
||||||
|
<span style="font-size:13px;font-weight:800;color:var(--accent);flex-shrink:0;">${v.total}</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:5px;height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;">
|
||||||
|
<div style="height:100%;width:${Math.max(pct,v.total?2:0)}%;background:var(--accent);border-radius:3px;transition:width .4s;"></div>
|
||||||
|
</div>
|
||||||
|
${v.total > 0 ? usersList : ''}
|
||||||
|
</div>`;
|
||||||
|
}).join('')}</div>`;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue