diff --git a/public/js/statistics.js b/public/js/statistics.js
index bfd162c..50d325c 100644
--- a/public/js/statistics.js
+++ b/public/js/statistics.js
@@ -2,10 +2,11 @@
// СТРАНИЦА СТАТИСТИКИ
// ══════════════════════════════════════════════════════════════════
-let statsPageOpen = false;
-let clientsExpanded = false;
-let clientsOnlyOpen = false;
-let timingPeriodIdx = 4;
+let statsPageOpen = false;
+let clientsExpanded = false;
+let clientsOnlyOpen = false;
+let timingPeriodIdx = 4;
+let clientLastTicketMode = 'created'; // 'created' | 'updated'
const STATS_CACHE_KEY = 'intradesk_stats_html_v2';
function saveStatsCache() {
@@ -409,6 +410,18 @@ function renderStatsPage() {
+
+
+
+
🕐 Последняя заявка по клиентам
+
+
+
+
+
+
+
+
@@ -549,6 +562,7 @@ function renderStatsPage() {
`;
renderClientsList();
+ renderClientLastTicketCard();
saveStatsCache();
}
@@ -606,7 +620,7 @@ 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; return'
'+escHtml(name)+'
'+v.count+''+(v.open?''+v.open+' откр.':'')+'
'; }).join('')}
${!clientsExpanded&&hidden>0?`+ ещё ${hidden} — показать все
`:''}`;
}
@@ -640,3 +654,59 @@ function hideLineTip(){const t=document.getElementById('line-tooltip');if(t)t.st
function openTaskById(id){const i=allData.findIndex(x=>x.task.id===id);if(i!==-1){closeStatsPage();setTimeout(()=>openDetail(i),100);}}
function onDataLoaded(){if(statsPageOpen)renderStatsPage();}
+
+function goToClientTasks(clientName) {
+ const filterVal = (clientName === 'Неизвестно') ? '__UNKNOWN__' : clientName;
+ closeStatsPage();
+ refreshAdvancedFilters();
+ const sel = document.getElementById('filter-object-select');
+ if (sel) {
+ [...sel.options].forEach(opt => { opt.selected = String(opt.value) === filterVal; });
+ updateAdvancedFilterLabel('filter-object-select');
+ buildAdvancedFilterPanel('filter-object-select');
+ }
+ renderTable();
+}
+
+function fmtAgo(ms) {
+ const s = Math.round(ms / 1000);
+ if (s < 60) return `${s} сек назад`;
+ const m = Math.round(s / 60);
+ if (m < 60) return `${m} мин назад`;
+ const h = Math.round(m / 60);
+ if (h < 24) return `${h} ч назад`;
+ const d = Math.round(h / 24);
+ if (d < 30) return `${d} дн назад`;
+ const mo = Math.round(d / 30);
+ if (mo < 12) return `${mo} мес назад`;
+ return `${Math.round(mo / 12)} г назад`;
+}
+
+function renderClientLastTicketCard() {
+ const wrap = document.getElementById('stats-client-last-ticket-wrap');
+ if (!wrap) return;
+ const now = Date.now();
+ const cMap = {};
+ allData.forEach(({task: t}) => {
+ const name = t._clientname || t.clientname || 'Неизвестно';
+ const cr = t.createdat ? new Date(t.createdat).getTime() : 0;
+ const upd = t.updatedat ? new Date(t.updatedat).getTime() : 0;
+ if (!cMap[name]) cMap[name] = { created: 0, updated: 0 };
+ if (cr > cMap[name].created) cMap[name].created = cr;
+ if (upd > cMap[name].updated) cMap[name].updated = upd;
+ });
+ const field = clientLastTicketMode;
+ const entries = Object.entries(cMap)
+ .filter(([, v]) => v[field] > 0)
+ .sort((a, b) => b[1][field] - a[1][field]);
+ if (!entries.length) { wrap.innerHTML = 'Нет данных
'; return; }
+ const maxAge = now - entries[entries.length - 1][1][field];
+ wrap.innerHTML = `${entries.map(([name, v]) => {
+ const ts = v[field];
+ const age = now - ts;
+ const pct = maxAge > 0 ? Math.max((1 - age / maxAge) * 100, 2) : 100;
+ const color = age < 86400000 ? '#0d9488' : age < 7 * 86400000 ? 'var(--accent)' : age < 30 * 86400000 ? '#d97706' : 'var(--red)';
+ const dateStr = new Date(ts).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' });
+ return `
${escHtml(name)}
${fmtAgo(age)}
${dateStr}
`;
+ }).join('')}
`;
+}
diff --git a/public/js/ui.js b/public/js/ui.js
index 37b9735..0a4af68 100644
--- a/public/js/ui.js
+++ b/public/js/ui.js
@@ -256,6 +256,7 @@ function refreshAdvancedFilters(){
}
const objectName=String(task._clientname||task.clientname||task.organization||'').trim();
if(objectName) objectMap.set(objectName, objectName);
+ else objectMap.set('__UNKNOWN__', 'Неизвестно');
const intraId=String(task.statusid||task.status||'').trim();
const intraName=String(task._statusname||task.statusname||intraId).trim();
if(intraId) intraMap.set(intraId, intraName||intraId);
@@ -593,7 +594,11 @@ function renderTable(keepPage){
if(selectedExecutors.size&& !selectedExecutors.has(String(item.task.executor||''))) return false;
if(selectedObjects.size){
const objectName=String(item.task._clientname||item.task.clientname||item.task.organization||'').trim();
- if(!objectName || !selectedObjects.has(objectName)) return false;
+ if(!objectName){
+ if(!selectedObjects.has('__UNKNOWN__')) return false;
+ } else {
+ if(!selectedObjects.has(objectName)) return false;
+ }
}
if(selectedIntra.size){
const intraId=String(item.task.statusid||item.task.status||'').trim();