diff --git a/public/css/app.css b/public/css/app.css index f6a4396..19a5189 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -1037,21 +1037,28 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } } /* ── Open-tickets week/month compare ────────────────────────── */ +.stats-open-wrap { display:flex; gap:16px; align-items:flex-start; } +.stats-open-chart { flex:1; min-width:0; } .stats-open-compare { display: flex; - align-items: center; - gap: 12px; - margin: 6px 0 12px; - flex-wrap: wrap; + flex-direction: column; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: var(--radius); + flex-shrink: 0; + min-width: 108px; + overflow: hidden; } .soc-item { display: flex; flex-direction: column; align-items: center; - gap: 2px; + gap: 3px; + padding: 12px 16px; } +.soc-divider { height:1px; background:var(--border); } .soc-val { - font-size: 22px; + font-size: 24px; font-weight: 800; color: var(--text); line-height: 1; @@ -1061,13 +1068,9 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } color: var(--text3); text-transform: uppercase; letter-spacing: .04em; + white-space: nowrap; } -.soc-sep { - color: var(--text3); - font-size: 13px; - font-weight: 600; - align-self: center; -} +.soc-sep { display: none; } .stats-wdiff { font-size: 11px; font-weight: 700; @@ -1080,7 +1083,35 @@ html.dark .stats-hm-cell:hover span { color: rgba(255,255,255,.95); } .stats-wdiff-down { background: color-mix(in srgb,var(--green) 15%,transparent); color: var(--green); } .stats-wdiff-flat { background: var(--surface2); color: var(--text3); } -/* ── Activity tooltip ───────────────────────────────────────── */ +/* ── Activity hover label ────────────────────────────────────── */ +.stats-act-hover-label { + position: absolute; + top: -20px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 2px; + font-size: 9px; + font-weight: 700; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity .15s; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 4px; + padding: 2px 5px; + z-index: 10; +} +.stats-activity-col:hover .stats-act-hover-label { opacity: 1; } +.stats-activity-col:hover .stats-activity-bar { filter: brightness(1.2) saturate(1.15); } +.stats-activity-bar { transition: filter .15s, height .2s; } +.sahl-cr { color: var(--accent); } +.sahl-sep { color: var(--text3); margin: 0 1px; } +.sahl-cl { color: var(--green); } + +/* ── Activity tooltip (floating, kept for other use) ─────────── */ .stats-act-tooltip { display: none; position: fixed; diff --git a/public/js/statistics.js b/public/js/statistics.js index 9196f51..b6acf2b 100644 --- a/public/js/statistics.js +++ b/public/js/statistics.js @@ -169,6 +169,16 @@ function renderStatsPage() { const HM_ORDER = [1,2,3,4,5,6,0]; const HM_LABELS = ['Пн','Вт','Ср','Чт','Пт','Сб','Вс']; + // Тепловая карта по месяцам (месяц × день недели) + const MONTH_HM = Array.from({length:12}, () => new Array(7).fill(0)); + data.forEach(({task:t}) => { + if (!t.createdat) return; + const d = new Date(t.createdat); + MONTH_HM[d.getMonth()][d.getDay()]++; + }); + const monthHmMax = Math.max(...MONTH_HM.flat(), 1); + const MONTH_NAMES = ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек']; + // 4. Без исполнителя const noExec = data.filter(({task:t}) => !t.executor).length; const noExecPct = total ? Math.round(noExec/total*100) : 0; @@ -362,31 +372,35 @@ function renderStatsPage() {
📈 Открытых заявок на конец недели
-
-
-
${wOpen[WEEKS-1]}
-
сейчас
+
+
+
+ + + + ${(()=>{ if(maxOpen===0)return''; const pts=wOpen.map((v,i)=>[i*60+30,115-v/maxOpen*95]); const area='M'+pts[0]+' '+pts.slice(1).map(p=>'L'+p).join(' ')+' L'+pts[pts.length-1][0]+',115 L'+pts[0][0]+',115 Z'; const line='M'+pts[0]+' '+pts.slice(1).map(p=>'L'+p).join(' '); return''+pts.map(([x,y],i)=>'').join(''); })()} + +
${wLabels.map((l,i)=>i%2===0?`${l}`:'').join('')}
+
-
vs
-
-
${wOpen[WEEKS-2]}
-
неделю назад
- ${wDiffBadge(wOpen[WEEKS-1]-wOpen[WEEKS-2])} +
+
+
${wOpen[WEEKS-1]}
+
сейчас
+
+
+
+
${wOpen[WEEKS-2]}
+
неделю назад
+ ${wDiffBadge(wOpen[WEEKS-1]-wOpen[WEEKS-2])} +
+
+
+
${openAtMonthStart}
+
нач. месяца
+ ${wDiffBadge(wOpen[WEEKS-1]-openAtMonthStart)} +
-
·
-
-
${openAtMonthStart}
-
нач. месяца
- ${wDiffBadge(wOpen[WEEKS-1]-openAtMonthStart)} -
-
-
- - - - ${(()=>{ if(maxOpen===0)return''; const pts=wOpen.map((v,i)=>[i*60+30,115-v/maxOpen*95]); const area='M'+pts[0]+' '+pts.slice(1).map(p=>'L'+p).join(' ')+' L'+pts[pts.length-1][0]+',115 L'+pts[0][0]+',115 Z'; const line='M'+pts[0]+' '+pts.slice(1).map(p=>'L'+p).join(' '); return''+pts.map(([x,y],i)=>'').join(''); })()} - -
${wLabels.map((l,i)=>i%2===0?`${l}`:'').join('')}
@@ -398,7 +412,7 @@ function renderStatsPage() { закрыто
- ${wCreated.map((cr,i)=>{ const cl=wClosed[i],hCr=Math.round(cr/maxDual*100),hCl=Math.round(cl/maxDual*100); return'
'+(cr||cl?''+cr+'':'')+'
'+wLabels[i]+'
'; }).join('')} + ${wCreated.map((cr,i)=>{ const cl=wClosed[i],hCr=Math.round(cr/maxDual*100),hCl=Math.round(cl/maxDual*100); return'
'+(cr||cl?''+cr+'/'+cl+'':'')+'
'+wLabels[i]+'
'; }).join('')}
@@ -435,21 +449,40 @@ function renderStatsPage() { - -
-
🌡️ Тепловая карта активности (час × день недели)
-
-
- ${Array.from({length:24},(_,h)=>`
${h}
`).join('')} + +
+
+
🌡️ Тепловая карта активности (час × день недели)
+
+
+ ${Array.from({length:24},(_,h)=>`
${h}
`).join('')} +
+ ${HM_ORDER.map((dayIdx,i)=>{ + const row=HM[dayIdx]; + return'
'+HM_LABELS[i]+'
'+row.map((v,h)=>'
'+(v||'')+'
').join('')+'
'; + }).join('')} +
+ меньше + ${[0,.2,.4,.6,.8,1].map(hi=>`
`).join('')} + больше +
- ${HM_ORDER.map((dayIdx,i)=>{ - const row=HM[dayIdx]; - return'
'+HM_LABELS[i]+'
'+row.map((v,h)=>'
'+(v||'')+'
').join('')+'
'; - }).join('')} -
- меньше - ${[0,.2,.4,.6,.8,1].map(hi=>`
`).join('')} - больше +
+
+
🗓️ По месяцам (месяц × день нед.)
+
+
+ ${HM_ORDER.map((_,i)=>`
${HM_LABELS[i]}
`).join('')} +
+ ${MONTH_NAMES.map((ml,mi)=>{ + const row=MONTH_HM[mi]; + return'
'+ml+'
'+HM_ORDER.map((di,dii)=>{const v=row[di];return'
'+(v||'')+'
';}).join('')+'
'; + }).join('')} +
+ меньше + ${[0,.2,.4,.6,.8,1].map(hi=>`
`).join('')} + больше +
@@ -628,7 +661,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} — показать все
`:''}`; } @@ -656,6 +689,7 @@ function donutLegend(sc,uc,tot){ } function escHtml(s){return String(s).replace(/&/g,'&').replace(//g,'>');} +function escAttr(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} function wDiffBadge(diff){ if(diff===0) return '= 0';