statistica vibe 4
This commit is contained in:
parent
de69bddcaf
commit
bcd1c40783
2 changed files with 52 additions and 12 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
### Улучшения
|
### Улучшения
|
||||||
|
|
||||||
|
- **График открытых заявок** — переключатель **«По неделям»** / **«По дням»**: 16 недель (как раньше) или 112 дней с тем же смыслом точки (снимок «сколько было открыто» на момент); боковая панель сравнения в режиме дней подписывает вторую строку «день назад».
|
||||||
- **Тепловая карта по месяцам** переработана: вместо сетки месяц×день-недели теперь отображается одна строка из 12 ячеек (по одной на месяц), каждая окрашена по числу заявок. Значение показывается при наведении.
|
- **Тепловая карта по месяцам** переработана: вместо сетки месяц×день-недели теперь отображается одна строка из 12 ячеек (по одной на месяц), каждая окрашена по числу заявок. Значение показывается при наведении.
|
||||||
- **Блок сравнения** («сейчас / неделю назад / начало месяца») теперь растягивается в высоту графика (`align-items: stretch`), элементы равномерно распределяются по высоте — цифры больше не выступают за пределы графика.
|
- **Блок сравнения** («сейчас / неделю назад / начало месяца») теперь растягивается в высоту графика (`align-items: stretch`), элементы равномерно распределяются по высоте — цифры больше не выступают за пределы графика.
|
||||||
- **Повторные клиенты** — один счётчик «открытых» через `isTaskOpen` (в т.ч. «в работе», «новая», «назначена» и т.п.); одна акцентная полоса и бейдж `N откр.` без разделения цветов.
|
- **Повторные клиенты** — один счётчик «открытых» через `isTaskOpen` (в т.ч. «в работе», «новая», «назначена» и т.п.); одна акцентная полоса и бейдж `N откр.` без разделения цветов.
|
||||||
|
|
@ -19,7 +20,7 @@
|
||||||
|
|
||||||
| Файл | Изменения |
|
| Файл | Изменения |
|
||||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `public/js/statistics.js` | `MONTH_COUNTS` вместо `MONTH_HM`; новый HTML месячной тепловой карты; `renderClientsList` с единой логикой `open` и правкой `opPct` для `clientsOnlyOpen` |
|
| `public/js/statistics.js` | `MONTH_COUNTS` вместо `MONTH_HM`; новый HTML месячной тепловой карты; `renderClientsList` с единой логикой `open` и правкой `opPct` для `clientsOnlyOpen`; переключатель недели/дни для графика открытых (`openChartByDays`, `lineOpen` / `lineSlot`) |
|
||||||
| `public/css/app.css` | `.stats-month-hm`*; `align-items: stretch` в `.stats-open-wrap`; уменьшен `.soc-val`; стили бейджа клиентов без раздельных полос |
|
| `public/css/app.css` | `.stats-month-hm`*; `align-items: stretch` в `.stats-open-wrap`; уменьшен `.soc-val`; стили бейджа клиентов без раздельных полос |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ let statsPageOpen = false;
|
||||||
let clientsExpanded = false;
|
let clientsExpanded = false;
|
||||||
let clientsOnlyOpen = false;
|
let clientsOnlyOpen = false;
|
||||||
let timingPeriodIdx = 4;
|
let timingPeriodIdx = 4;
|
||||||
|
/** false = по неделям (16 точек), true = по дням (112 дней) — график «открытых заявок» */
|
||||||
|
let openChartByDays = false;
|
||||||
|
|
||||||
const STATS_CACHE_KEY = 'intradesk_stats_html_v2';
|
const STATS_CACHE_KEY = 'intradesk_stats_html_v2';
|
||||||
function saveStatsCache() {
|
function saveStatsCache() {
|
||||||
|
|
@ -93,9 +95,40 @@ function renderStatsPage() {
|
||||||
wCreated.push(data.filter(({task:t})=>{const c=new Date(t.createdat);return c>=wStart&&c<wEnd;}).length);
|
wCreated.push(data.filter(({task:t})=>{const c=new Date(t.createdat);return c>=wStart&&c<wEnd;}).length);
|
||||||
wClosed.push(data.filter(({task:t})=>{const c=new Date(t.resolutiondatefact||t.closedat||0);return c>=wStart&&c<wEnd&&isTaskDone({task:t});}).length);
|
wClosed.push(data.filter(({task:t})=>{const c=new Date(t.resolutiondatefact||t.closedat||0);return c>=wStart&&c<wEnd&&isTaskDone({task:t});}).length);
|
||||||
}
|
}
|
||||||
const maxOpen = Math.max(...wOpen,1);
|
const maxOpenWeekly = Math.max(...wOpen,1);
|
||||||
const maxDual = Math.max(...wCreated,...wClosed,1);
|
const maxDual = Math.max(...wCreated,...wClosed,1);
|
||||||
|
|
||||||
|
// Линия «открытых»: те же 16 недель или помесячно-эквивалентно 112 дням
|
||||||
|
const OPEN_CHART_DAYS = 112;
|
||||||
|
let lineLabels, lineOpen, lineN, lineSlot, maxOpen, lineDotR, lineLabelStep;
|
||||||
|
if (openChartByDays) {
|
||||||
|
lineN = OPEN_CHART_DAYS;
|
||||||
|
lineSlot = 6;
|
||||||
|
lineDotR = 3;
|
||||||
|
lineLabelStep = 14;
|
||||||
|
lineLabels = [];
|
||||||
|
lineOpen = [];
|
||||||
|
for (let i = lineN - 1; i >= 0; i--) {
|
||||||
|
const tEnd = new Date(now - i * 86400000);
|
||||||
|
lineLabels.push(tEnd.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' }));
|
||||||
|
lineOpen.push(data.filter(({ task: t }) => {
|
||||||
|
const cr = new Date(t.createdat);
|
||||||
|
if (cr >= tEnd) return false;
|
||||||
|
if (!isTaskDone({ task: t })) return true;
|
||||||
|
return new Date(t.resolutiondatefact || t.closedat || 0) > tEnd;
|
||||||
|
}).length);
|
||||||
|
}
|
||||||
|
maxOpen = Math.max(...lineOpen, 1);
|
||||||
|
} else {
|
||||||
|
lineLabels = wLabels;
|
||||||
|
lineOpen = wOpen;
|
||||||
|
lineN = WEEKS;
|
||||||
|
lineSlot = 60;
|
||||||
|
lineDotR = 5;
|
||||||
|
lineLabelStep = 2;
|
||||||
|
maxOpen = maxOpenWeekly;
|
||||||
|
}
|
||||||
|
|
||||||
// Открытых на начало текущего месяца
|
// Открытых на начало текущего месяца
|
||||||
const firstDayOfMonth = new Date(now); firstDayOfMonth.setDate(1); firstDayOfMonth.setHours(0,0,0,0);
|
const firstDayOfMonth = new Date(now); firstDayOfMonth.setDate(1); firstDayOfMonth.setHours(0,0,0,0);
|
||||||
const openAtMonthStart = data.filter(({task:t})=>{ const cr=new Date(t.createdat); if(cr>=firstDayOfMonth)return false; if(!isTaskDone({task:t}))return true; return new Date(t.resolutiondatefact||t.closedat||0)>firstDayOfMonth; }).length;
|
const openAtMonthStart = data.filter(({task:t})=>{ const cr=new Date(t.createdat); if(cr>=firstDayOfMonth)return false; if(!isTaskDone({task:t}))return true; return new Date(t.resolutiondatefact||t.closedat||0)>firstDayOfMonth; }).length;
|
||||||
|
|
@ -368,36 +401,42 @@ function renderStatsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Открытые на конец недели -->
|
<!-- Открытых заявок (недели / дни) -->
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<div class="stats-card-title">📈 Открытых заявок на конец недели</div>
|
<div class="stats-card-title" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
|
||||||
|
<span>📈 Открытых заявок ${openChartByDays ? 'по дням' : 'на конец недели'}</span>
|
||||||
|
<div class="stats-period-tabs">
|
||||||
|
<button type="button" class="stats-period-tab${openChartByDays?'':' active'}" onclick="openChartByDays=false;renderStatsPage()">По неделям</button>
|
||||||
|
<button type="button" class="stats-period-tab${openChartByDays?' active':''}" onclick="openChartByDays=true;renderStatsPage()">По дням</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="stats-open-wrap">
|
<div class="stats-open-wrap">
|
||||||
<div class="stats-open-chart">
|
<div class="stats-open-chart">
|
||||||
<div class="stats-line-chart" id="stats-line-chart">
|
<div class="stats-line-chart" id="stats-line-chart">
|
||||||
<div class="stats-line-tooltip" id="line-tooltip" style="display:none;"></div>
|
<div class="stats-line-tooltip" id="line-tooltip" style="display:none;"></div>
|
||||||
<svg class="stats-svg" viewBox="0 0 ${WEEKS*60} 130" preserveAspectRatio="none">
|
<svg class="stats-svg" viewBox="0 0 ${lineN*lineSlot} 130" preserveAspectRatio="none">
|
||||||
<defs><linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="var(--accent)" stop-opacity="0.18"/><stop offset="100%" stop-color="var(--accent)" stop-opacity="0.01"/></linearGradient></defs>
|
<defs><linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="var(--accent)" stop-opacity="0.18"/><stop offset="100%" stop-color="var(--accent)" stop-opacity="0.01"/></linearGradient></defs>
|
||||||
${(()=>{ 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'<path d="'+area+'" fill="url(#lineGrad)"/><path d="'+line+'" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>'+pts.map(([x,y],i)=>'<circle class="line-dot" cx="'+x+'" cy="'+y+'" r="5" fill="var(--accent)" stroke="var(--surface)" stroke-width="2" data-val="'+wOpen[i]+'" data-lbl="'+wLabels[i]+'" onmouseenter="showLineTip(event,this)" onmouseleave="hideLineTip()"/>').join(''); })()}
|
${(()=>{ if(maxOpen===0)return''; const cx=i=>i*lineSlot+lineSlot/2; const pts=lineOpen.map((v,i)=>[cx(i),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'<path d="'+area+'" fill="url(#lineGrad)"/><path d="'+line+'" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>'+pts.map(([x,y],i)=>'<circle class="line-dot" cx="'+x+'" cy="'+y+'" r="'+lineDotR+'" fill="var(--accent)" stroke="var(--surface)" stroke-width="2" data-val="'+lineOpen[i]+'" data-lbl="'+lineLabels[i]+'" onmouseenter="showLineTip(event,this)" onmouseleave="hideLineTip()"/>').join(''); })()}
|
||||||
</svg>
|
</svg>
|
||||||
<div class="stats-line-xlabels">${wLabels.map((l,i)=>i%2===0?`<span>${l}</span>`:'<span></span>').join('')}</div>
|
<div class="stats-line-xlabels">${lineLabels.map((l,i)=>i%lineLabelStep===0?`<span>${l}</span>`:'<span></span>').join('')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-open-compare">
|
<div class="stats-open-compare">
|
||||||
<div class="soc-item">
|
<div class="soc-item">
|
||||||
<div class="soc-val">${wOpen[WEEKS-1]}</div>
|
<div class="soc-val">${lineOpen[lineN-1]}</div>
|
||||||
<div class="soc-label">сейчас</div>
|
<div class="soc-label">сейчас</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="soc-divider"></div>
|
<div class="soc-divider"></div>
|
||||||
<div class="soc-item">
|
<div class="soc-item">
|
||||||
<div class="soc-val">${wOpen[WEEKS-2]}</div>
|
<div class="soc-val">${lineN>=2?lineOpen[lineN-2]:'—'}</div>
|
||||||
<div class="soc-label">неделю назад</div>
|
<div class="soc-label">${openChartByDays?'день назад':'неделю назад'}</div>
|
||||||
${wDiffBadge(wOpen[WEEKS-1]-wOpen[WEEKS-2])}
|
${lineN>=2?wDiffBadge(lineOpen[lineN-1]-lineOpen[lineN-2]):''}
|
||||||
</div>
|
</div>
|
||||||
<div class="soc-divider"></div>
|
<div class="soc-divider"></div>
|
||||||
<div class="soc-item">
|
<div class="soc-item">
|
||||||
<div class="soc-val">${openAtMonthStart}</div>
|
<div class="soc-val">${openAtMonthStart}</div>
|
||||||
<div class="soc-label">нач. месяца</div>
|
<div class="soc-label">нач. месяца</div>
|
||||||
${wDiffBadge(wOpen[WEEKS-1]-openAtMonthStart)}
|
${wDiffBadge(lineOpen[lineN-1]-openAtMonthStart)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue