График
Сводка
Здесь будут итоги: сумма, среднее, медиана, минимум/максимум.
Таблица (агрегированные данные)
<?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;i