...
[crm_analytics]

Аналитика отчётов

График

Сводка

Здесь будут итоги: сумма, среднее, медиана, минимум/максимум.

Таблица (агрегированные данные)

ГруппаЗначение
<?php $html = ob_get_clean(); // Подключаем библиотеки и наш JS-хук wp_enqueue_script('sheetjs'); wp_enqueue_script('chartjs'); wp_enqueue_script('crm-analytics-main'); // Основной JS: устойчивый инициализатор + логика $js = <<<'JS' (function(){ // === УСТОЙЧИВАЯ ИНИЦИАЛИЗАЦИЯ ДЛЯ ELEMENTOR === function ready(fn){ if(document.readyState!=='loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } function initOnce(root){ if (!root || root.dataset.crmAnalyticsInit === '1') return; root.dataset.crmAnalyticsInit = '1'; const version = 'v3.0'; const q = sel => root.querySelector(sel); const els = { file: q('.anaFile'), metric: q('.anaMetric'), groupBy: q('.anaGroupBy'), rowFilter: q('.anaRowFilter'), analyze: q('.anaAnalyze'), summaryBtn: q('.anaSummary'), reset: q('.anaReset'), info: q('.anaInfo'), headerChips: q('.anaHeaderChips'), debug: q('.anaDebug'), chartCanvas: q('.anaChart'), tableBody: q('.anaTableBody'), summaryBox: q('.anaSummaryBox'), }; // Диагностика окружения function diag(msg){ const lines = [ `CRM Analytics ${version}`, `XLSX: ${typeof XLSX !== 'undefined' ? 'OK' : 'NOT LOADED'}`, `Chart: ${typeof Chart !== 'undefined' ? 'OK' : 'NOT LOADED'}`, msg||'' ]; els.debug.textContent = lines.join(' | '); } diag('Init…'); // Если библиотеки не подгрузились — покажем сообщение и выйдем (не блокируем весь сайт) if (typeof XLSX === 'undefined' || typeof Chart === 'undefined') { if (els.info) { els.info.textContent = 'Библиотеки не загрузились. Проверь кэш/блокировки CDN.'; els.info.className = 'crm-ana-note crm-ana-bad'; } diag('Libs missing'); return; } let rawRows = []; let headers = []; let chart; function setInfo(msg, bad=false){ els.info.textContent = msg||''; els.info.className='crm-ana-note ' + (bad?'crm-ana-bad':'crm-ana-good'); } function escapeHtml(s){ return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function normHeader(x){ let s = String(x??''); s = s.replace(/^\uFEFF/, '').replace(/\u00A0/g,' ').replace(/\s+/g,' ').trim(); return s; } function dedupeHeaders(arr){ const seen = new Map(); return arr.map(h=>{ let name = h || 'Колонка'; let base = name, i=1; while(seen.has(name)){ i++; name = base + ' ('+i+')'; } seen.set(name,true); return name; }); } const headerHints = [ 'ID начисления','Дата начисления','Группа услуг','Тип начисления','Артикул','SKU', 'Название товара','Количество','Цена продавца','Дата принятия заказа','Схема работы', 'Сумма итого, руб.','Сумма итого','Логистика','Выручка','Комиссия' ]; function detectHeaderRow(rows2d){ let bestIdx=-1,bestScore=-1; for(let i=0;iString(v??'').trim()!=='').length; const hits=row.reduce((a,v)=>a+(headerHints.some(h=>String(v??'').toLowerCase().includes(h.toLowerCase()))?1:0),0); const score = filled + hits*3; if(score>bestScore){bestScore=score;bestIdx=i;} } return bestIdx; } function buildObjects(rows2d, headerRowIdx){ const head = (rows2d[headerRowIdx]||[]).map(normHeader); const cols = dedupeHeaders(head.map(h=>h||'Колонка')); const out = []; for(let r=headerRowIdx+1; rs.trim()).filter(Boolean); for(const p of parts){ let type,col,val; if(p.includes('!=')){[col,val]=p.split('!=').map(s=>s.trim()); type='neq';} else if(p.includes('~')){[col,val]=p.split('~').map(s=>s.trim()); type='like';} else if(p.includes('=')){[col,val]=p.split('=').map(s=>s.trim()); type='eq';} if(col&&val) rules.push({type,col,val}); } return rules; } function passRowFilters(row,rules){ for(const r of rules){ const cell=(row[r.col]??'').toString(); if(r.type==='eq' && cell!==r.val) return false; if(r.type==='neq' && cell===r.val) return false; if(r.type==='like' && !cell.toLowerCase().includes(r.val.toLowerCase())) return false; } return true; } function toNumber(x){ // Excel-даты в числах не считаем метрикой if (typeof x === 'number' && x > 20000 && x < 60000) return NaN; if(typeof x==='number') return x; if(x==null) return NaN; let s=String(x).trim(); if(!s) return NaN; s=s.replace(/\s|\u00A0/g,''); const lastComma=s.lastIndexOf(','), lastDot=s.lastIndexOf('.'); if(lastComma>lastDot){ s=s.replace(/\./g,'').replace(',', '.'); } else { s=s.replace(/,/g,''); } const n=parseFloat(s); return Number.isFinite(n)?n:NaN; } function stats(arr){ const n=arr.length; if(!n) return {count:0,sum:0,avg:0,med:0,min:0,max:0}; const sum=arr.reduce((a,b)=>a+b,0), avg=sum/n; const sorted=[...arr].sort((a,b)=>a-b), mid=Math.floor(n/2); const med=n%2?sorted[mid]:(sorted[mid-1]+sorted[mid])/2; return {count:n,sum,avg,med,min:sorted[0],max:sorted[n-1]}; } function formatNumber(n){ return Number.isFinite(n)? new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(n) : '-'; } function populateSelectors(){ const opts=headers.map(h=>``).join(''); els.metric.innerHTML = `${opts}`; els.groupBy.innerHTML = `${opts}`; els.metric.disabled=false; els.groupBy.disabled=false; els.rowFilter.disabled=false; els.analyze.disabled=false; els.summaryBtn.disabled=false; els.reset.disabled=false; // Автовыбор популярных колонок const pick = (cands)=> cands.find(n=>headers.some(h=>h.toLowerCase().includes(n.toLowerCase()))); const metricGuess = pick(['Сумма итого, руб.','Сумма итого','Логистика','Выручка','Комиссия']); const groupGuess = pick(['Дата начисления','Дата','Тип начисления','Группа услуг']); if(metricGuess){ els.metric.value = headers.find(h=>h.toLowerCase().includes(metricGuess.toLowerCase())); } if(groupGuess){ els.groupBy.value = headers.find(h=>h.toLowerCase().includes(groupGuess.toLowerCase())); } } function renderHeaderChips(){ els.headerChips.innerHTML = headers.length ? headers.map(h=>`${escapeHtml(h)}`).join('') : ''; } function renderTable(pairs){ els.tableBody.innerHTML=''; if(!pairs.length){ els.tableBody.innerHTML='Нет данных для отображения'; return; } for(const [key,val] of pairs){ const tr=document.createElement('tr'); tr.innerHTML = `${escapeHtml(key)}${formatNumber(val)}`; els.tableBody.appendChild(tr); } } function renderChart(labels,values,labelName){ if(chart) chart.destroy(); chart = new Chart(els.chartCanvas,{ type:'bar', data:{labels, datasets:[{label:labelName||'Значение', data:values}]}, options:{ responsive:true, animation:false, scales:{ x:{ticks:{autoSkip:true,maxRotation:0}}, y:{beginAtZero:true} }, plugins:{ legend:{display:true}, tooltip:{callbacks:{ label:ctx=>`${ctx.dataset.label}: ${formatNumber(ctx.parsed.y)}` }} } } }); } function resetAll(){ rawRows=[]; headers=[]; els.metric.innerHTML=''; els.groupBy.innerHTML=''; els.metric.disabled=true; els.groupBy.disabled=true; els.rowFilter.value=''; els.rowFilter.disabled=true; els.analyze.disabled=true; els.summaryBtn.disabled=true; els.reset.disabled=true; els.tableBody.innerHTML=''; els.headerChips.innerHTML=''; els.summaryBox.textContent='Здесь будут итоги: сумма, среднее, медиана, минимум/максимум.'; if(chart){ chart.destroy(); chart=null; } setInfo(''); } resetAll(); // === Обработчик загрузки файла === els.file.addEventListener('change', async ()=>{ resetAll(); const f = els.file.files && els.file.files[0]; if(!f) return; setInfo('Загрузка файла…'); diag('Reading…'); try{ const buf = await f.arrayBuffer(); const wb = XLSX.read(buf, {type:'array', raw:false}); const sheet = wb.SheetNames[0]; const ws = wb.Sheets[sheet]; // Сырая матрица const rows2d = XLSX.utils.sheet_to_json(ws, {header:1, defval:''}); if(!rows2d.length){ setInfo('Файл пустой или не распознан.', true); diag('Empty'); return; } const headerRowIdx = detectHeaderRow(rows2d); if(headerRowIdx<0){ setInfo('Не удалось найти строку с заголовками.', true); diag('No header'); return; } const built = buildObjects(rows2d, headerRowIdx); headers = dedupeHeaders(built.headers.map(normHeader)); rawRows = built.rows; if(!headers.length || !rawRows.length){ setInfo('Данные не найдены после заголовка. Проверьте файл.', true); diag('No data'); return; } populateSelectors(); renderHeaderChips(); setInfo(`Лист: ${sheet}. Загружено строк: ${rawRows.length}.`); diag(`Ready: ${rawRows.length} rows`); }catch(e){ console.error(e); setInfo('Ошибка чтения файла. Проверь формат CSV/XLSX.', true); diag('Read error'); } }); // === Построить диаграмму === els.analyze.addEventListener('click', ()=>{ const metricCol = els.metric.value, groupCol = els.groupBy.value; if(!metricCol || !groupCol){ setInfo('Выберите столбец метрики и столбец группировки.', true); return; } const rules = parseRowFilters(els.rowFilter.value); const map = new Map(); for(const r of rawRows){ if(!passRowFilters(r, rules)) continue; const keyRaw = r[groupCol]; let key = (keyRaw==null || keyRaw==='') ? '(пусто)' : String(keyRaw); // Excel-даты иногда прилетают как Date-объекты if (Object.prototype.toString.call(keyRaw)==='[object Date]' && !isNaN(keyRaw)) { const d=new Date(keyRaw); key = d.toISOString().slice(0,10); } const val = toNumber(r[metricCol]); if(!Number.isFinite(val)) continue; map.set(key, (map.get(key)||0)+val); } const entries = [...map.entries()]; const looksLikeDate = entries.length && entries.every(([k]) => !isNaN(Date.parse(k))); if(looksLikeDate) entries.sort((a,b)=> new Date(a[0]) - new Date(b[0])); else entries.sort((a,b)=> a[0].localeCompare(b[0], undefined, {numeric:true, sensitivity:'base'})); const labels = entries.map(e=>e[0]); const values = entries.map(e=>e[1]); if(!values.length){ renderTable([]); if(chart){chart.destroy();chart=null;} setInfo('Нет числовых данных в выбранной метрике после фильтра.', true); return; } renderChart(labels, values, metricCol); renderTable(entries); const allVals=[]; for(const r of rawRows){ if(!passRowFilters(r, rules)) continue; const v=toNumber(r[metricCol]); if(Number.isFinite(v)) allVals.push(v); } const s = stats(allVals); els.summaryBox.innerHTML = `Строк (учтено): ${s.count}
Сумма: ${formatNumber(s.sum)}
Среднее: ${formatNumber(s.avg)}
Медиана: ${formatNumber(s.med)}
Минимум: ${formatNumber(s.min)}
Максимум: ${formatNumber(s.max)}`; setInfo(`Готово. Точек на графике: ${labels.length}.`); diag('Chart OK'); }); // === Сводка === els.summaryBtn.addEventListener('click', ()=>{ const metricCol = els.metric.value; if(!metricCol){ setInfo('Выберите столбец метрики.', true); return; } const rules = parseRowFilters(els.rowFilter.value); const vals=[]; for(const r of rawRows){ if(!passRowFilters(r, rules)) continue; const v=toNumber(r[metricCol]); if(Number.isFinite(v)) vals.push(v); } const s = stats(vals); els.summaryBox.innerHTML = `Строк (учтено): ${s.count}
Сумма: ${formatNumber(s.sum)}
Среднее: ${formatNumber(s.avg)}
Медиана: ${formatNumber(s.med)}
Минимум: ${formatNumber(s.min)}
Максимум: ${formatNumber(s.max)}`; setInfo('Сводка обновлена.'); diag('Summary OK'); }); els.reset.addEventListener('click', resetAll); } // Ждём DOM и сам элемент. Это лечит отложенный рендер Elementor/темой. ready(function(){ // 1) Пробуем инициализировать сразу все блоки на странице document.querySelectorAll('.crm-analytics').forEach(initOnce); // 2) Если контент дорисовывается позднее — MutationObserver отловит вставку const obs = new MutationObserver(() => { document.querySelectorAll('.crm-analytics').forEach(initOnce); }); obs.observe(document.documentElement, {childList:true, subtree:true}); // 3) Доп. страховка таймером (некоторые оптимизаторы переносят скрипты) let tries=0; const tick = setInterval(() => { document.querySelectorAll('.crm-analytics').forEach(initOnce); tries++; if(tries>20) clearInterval(tick); // 20 раз по ~1с — хватит }, 1000); }); })(); JS; wp_add_inline_script('crm-analytics-main', $js); return $html; });
KWIZ разработан в web studio solomon
Заказать услугу
заказать интернет магазин
заказать корпоративный сайт
заказать сайт визитку
Получить консультацию
Заказать обратный звонок
Серафинит - АкселераторОптимизировано Серафинит - Акселератор
Включает высокую скорость сайта, чтобы быть привлекательным для людей и поисковых систем.