'use strict'; let selectedRecord = null; let selectedElem = null; const prettifyThing = (name, value, record = null, globalParms = null) => { if (name === 'source' || name === 'destination' || name === 'any') { return common.prettifyPhone(value); } else if (name === 'totalDuration' || name === 'callDuration') { return (globalParms.timeInSeconds ? value : common.convertToDuration(value)); } else if (name === 'dateStart' || name === 'dateEnd') { return dayjs(value).format('DD.MM.YYYY'); } else if (name === 'callDate') { return (globalParms.disableGrouping ? dayjs(value).format('DD.MM.YYYY HH:mm:ss') : dayjs(value).format('HH:mm:ss')); } else if (name === 'callResult') { return ['Отвечен', 'Не отвечен', 'Занято', 'Ошибка', 'Другое', 'Отменен'][value]; } else if (name === 'dailyCT') { return ['ВХ', 'ИСХ', 'ВН'][record.callType] + '-' + value; } return value; }; const uglifyThing = (name, value) => { if (name === 'source' || name === 'destination' || name === 'any') { return common.uglifyPhone(value); } else if (name === 'dateStart' || name === 'dateEnd') { return value.replace(/(\d{1,2})\.(\d{1,2})\.(\d+)/, '$3-$2-$1'); } else if (name === 'dailyID' || name === 'dailyCT' || name === 'id') { return value.replace(/\D/g, ''); } return value; }; const preferableSortDirs = { callDate: 'desc', source: 'asc', destination: 'asc', totalDuration: 'desc', callDuration: 'desc' }; const filterProperties = ['source', 'destination', 'any', 'dateStart', 'dateEnd', 'id', 'callTypes', 'wcSource', 'wcDestination', 'wcAny']; const sortProperties = ['sort', 'dir']; const getCdrParams = () => { let parms = {}; // specify page if its not the 1st one if (cdr.thisPage !== 1) { parms.page = cdr.thisPage; } // iterate over all known SORT and FILTER properties, add them if necessary [...sortProperties, ...filterProperties].forEach(e => { if (cdr.filter[e]) { parms[e] = uglifyThing(e, cdr.filter[e]); } }); // remove default sort options if (parms.sort === 'callDate' && parms.dir === 'desc') { delete parms.sort; delete parms.dir; } // check if grouping is disabled if (document.querySelector('.left-panel .settings .checkbox.grouping > input').checked) { parms.disableGrouping = 1; } return Object.keys(parms).map(e => e + '=' + parms[e]); }; const getCdr = async () => { try { let url = [...getCdrParams()].join('&'); url = url ? ('?' + url) : '/'; // update browser url if (history.pushState) { history.pushState({}, null, url); } const response = await fetch('/api/query' + url); if (!response.ok) { throw new Error(response.status, ' ', response.statusText); } return (await response.json()); } catch (e) { showMessage({ icon: 'icon-error', header: 'Ошибка загрузки данных', text: 'Не удалось загрузить данные: ' + e }); console.warn('Failed to fetch data: ', e) return null; } }; const getCallsSinceLastUpdate = async () => { try { if (cdr && cdr.latest) { const response = await fetch('/api/check-new?lastUpdateAt=' + dayjs(cdr.latest).unix()); if (response.ok) { return (await response.json()).numSinceLastUpdate; } } } catch (e) { console.warn('Failed to query last update API: ' + e); } }; const performUpdate = async () => { const count = await getCallsSinceLastUpdate(); if (count > 0) { if (document.querySelector('.left-panel .settings .checkbox.auto-update > input').checked) { document.querySelector('.left-panel .new-entries').classList.remove('active'); populate(); } else { document.querySelector('.left-panel .new-entries .num').innerHTML = count; document.querySelector('.left-panel .new-entries').classList.add('active'); } } updateTimerHandle = setTimeout(performUpdate, 5000); }; let updateTimerHandle = 0; const handlerDoUpdate = (ev, el) => { clearTimeout(updateTimerHandle); updateTimerHandle = setTimeout(performUpdate, 5000); document.querySelector('.left-panel .new-entries').classList.remove('active'); populate(); }; const columnNameToId = name => { switch (name) { case 'dailyid': { return 'dailyID'; } case 'dailyct': { return 'dailyCT'; } case 'date': { return 'callDate'; } case 'result': { return 'callResult'; } case 'total-duration': { return 'totalDuration'; } case 'call-duration': { return 'callDuration'; } case 'id': { return 'uniqueID'; } default: { return name; } } }; const addInfoToGroup = (group, cd, gp) => { const element = cdr.dailyStats.find( e => dayjs(e.day).isSame(cd, 'day')); if (element) { const stats = document.createElement('div'); stats.classList.add('stats'); const phone = createSVG(['image'], 'icon-phone'); const numCalls = document.createElement('span'); numCalls.classList.add('num-calls'); numCalls.innerHTML = element.totalCalls; const clock = createSVG(['image'], 'icon-clock'); const duration = document.createElement('span'); duration.innerHTML = prettifyThing('totalDuration', element.totalDuration, null, gp) + ' / ' + prettifyThing('callDuration', element.callDuration, null, gp); stats.appendChild(phone); stats.appendChild(numCalls); stats.appendChild(clock); stats.appendChild(duration); group.appendChild(stats); } } const tryNewGroup = (id, cdrBody, gp) => { if (id === 0 || !dayjs(cdr.records[id - 1].callDate).isSame(dayjs(cdr.records[id].callDate), 'day')) { const group = document.createElement('div'); group.classList.add('entry', 'group'); const groupText = document.createElement('span'); groupText.classList.add('group-text'); groupText.innerHTML = dayjs(cdr.records[id].callDate).format('DD.MM.YYYY'); if (cdr.isFirstCont && id === 0) { groupText.innerHTML += ' (продолжение)'; } group.appendChild(groupText); if (cdr.dailyStats) { addInfoToGroup(group, cdr.records[id].callDate, gp); } cdrBody.appendChild(group); } }; const draw = () => { if (!cdr) { throw new Error("Tried to draw without CDR data"); } // no-cdrs box document.querySelector('.cdr .body .no-cdrs').classList.toggle('active', !cdr.records || cdr.records.length === 0); // pages document.querySelector('.cdr .controls .status').innerHTML = `${cdr.thisPage} из ${cdr.maxPages}`; document.querySelector('.cdr .controls .goto-page').value = cdr.thisPage; document.querySelector('.cdr .controls .goto-page').max = cdr.maxPages; // page buttons document.querySelector('.cdr .controls .button_begin').classList.toggle('active', cdr.thisPage > 1); document.querySelector('.cdr .controls .button_left').classList.toggle('active', cdr.thisPage > 1); document.querySelector('.cdr .controls .button_right').classList.toggle('active', cdr.thisPage < cdr.maxPages); document.querySelector('.cdr .controls .button_end').classList.toggle('active', cdr.thisPage < cdr.maxPages); document.querySelector('.cdr .controls .button_goto').classList.toggle('active', cdr.maxPages > 1); document.querySelector('.cdr .controls .goto-page').classList.toggle('active', cdr.maxPages > 1); // filters document.querySelectorAll('.left-panel .filters .filter').forEach( e => { let id = e.getAttribute('filter-id'); if (id) { e.querySelector('.value').value = cdr.filter[id] ? prettifyThing(id, cdr.filter[id]) : ""; e.querySelector('.status').classList.toggle('active', !!cdr.filter[id]); } }); // call type filters document.querySelector('.left-panel .call-type.call-type_incoming').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctIncoming)); document.querySelector('.left-panel .call-type.call-type_outgoing').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctOutgoing)); document.querySelector('.left-panel .call-type.call-type_internal').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctInternal)); // wildcards document.querySelector('.left-panel .filters .filter .wildcard_source').classList.toggle('active', cdr.filter.wcSource); document.querySelector('.left-panel .filters .filter .wildcard_destination').classList.toggle('active', cdr.filter.wcDestination); document.querySelector('.left-panel .filters .filter .wildcard_any').classList.toggle('active', cdr.filter.wcAny); // sortable fields document.querySelectorAll('.cdr .header .cell.sortable').forEach( e => { let id = "" + e.getAttribute('sort-id'); if (id) { e.querySelector('.sort-button').classList.toggle('sort-none', cdr.filter.sort !== id); e.querySelector('.sort-button').classList.toggle('sort-asc', cdr.filter.sort === id && cdr.filter.dir === 'asc'); e.querySelector('.sort-button').classList.toggle('sort-desc', cdr.filter.sort === id && cdr.filter.dir === 'desc'); } }); // records // cache CDR body descriptor // (not actually cdr body due to scrollbars) const cdrBody = document.querySelector('.cdr .body .no-cdrs').parentElement; // cache some settings const globalParms = { timeInSeconds: document.querySelector('.left-panel .settings .checkbox.time-in-seconds > input').checked, disableGrouping: document.querySelector('.left-panel .settings .checkbox.grouping > input').checked }; // change date header if grouping is enabled document.querySelector('.cdr .header .cell.cell_date .name').innerHTML = globalParms.disableGrouping ? 'Дата' : 'Время'; // remove all records first cdrBody.querySelectorAll('.entry').forEach( e => { e.remove(); }) // iterate over records if there are any let idx = (cdr.thisPage - 1) * cdr.filter.limit + 1; let localIdx = 0; // clear out selectedElem selectedElem = null; cdr.records.forEach( record => { // if grouping is enabled, attempt to place a group header if (!globalParms.disableGrouping) { tryNewGroup(localIdx, cdrBody, globalParms); } let entry = document.createElement('div'); entry.classList.add('entry', 'record'); entry.addEventListener('click', wrapListener(handlerSelectRecord, entry, record.uniqueID)); // check if this entry has been previously selected if (selectedRecord === record.uniqueID) { entry.classList.add('active'); selectedElem = entry; } ['id', 'dailyct', 'type', 'date', 'source', 'destination', 'total-duration', 'call-duration', 'result', 'records'].forEach( name => { const cell = document.createElement('div'); cell.classList.add('cell', 'cell_' + name); switch (name) { case 'id': { cell.innerHTML = idx; break; } case 'type': { const ct = common.callTypeToString(record.callType); cell.appendChild(createSVG(['image', 'type_' + ct], 'icon-' + ct + '-call')) break; } case 'records': { let play = createSVG(['image', 'link', 'play'], 'icon-play'); play.addEventListener('click', wrapListener(handlerPlay, record.uniqueID)); cell.appendChild(play); let save = createSVG(['image', 'link', 'save'], 'icon-save'); save.addEventListener('click', wrapListener(handlerSave, record.uniqueID)); cell.appendChild(save); break; } default: { const colName = columnNameToId(name); cell.innerHTML = prettifyThing(colName, record[colName], record, globalParms); } } entry.appendChild(cell); }); cdrBody.appendChild(entry); idx++; localIdx++; }); // TODO group view }; const populate = async () => { const newCdr = await getCdr(); if (!newCdr) { return; } cdr = newCdr; draw(); }; function handlerSave(ev, uid) { if (uid) { window.location.assign('/api/recording/' + uid + '?asFile=true'); } ev.stopPropagation(); } function handlerSelectRecord(ev, el, id) { if (selectedRecord !== id && selectedElem) { selectedElem.classList.remove('active'); } selectedRecord = (selectedRecord === id) ? null : id; selectedElem = (selectedRecord !== null) ? el : null; el.classList.toggle('active'); } function handlerFilterNotify(ev, el) { el.parentElement.querySelector('.status').classList.toggle('active', el.value && el.value.length > 0); } document.querySelectorAll('.left-panel .filters .filter .value').forEach(e => e.addEventListener('change', wrapListener(handlerFilterNotify, e))); document.querySelectorAll('.left-panel .filters .filter .value').forEach(e => e.addEventListener('input', wrapListener(handlerFilterNotify, e))); function handlerSort(ev, el) { const id = "" + el.getAttribute('sort-id'); if (id) { if (cdr.filter.sort === id) { cdr.filter.dir = (cdr.filter.dir === 'asc') ? 'desc' : 'asc'; } else { cdr.thisPage = 1; cdr.filter.sort = id; cdr.filter.dir = preferableSortDirs[cdr.filter.sort] ? preferableSortDirs[cdr.filter.sort] : 'desc'; } } populate(); } function handlerBegin() { if (cdr.thisPage !== 1) { cdr.thisPage = 1; populate(); } } function handlerLeft() { if (cdr.thisPage > 1) { --cdr.thisPage; populate(); } } function handlerRight() { if (cdr.thisPage < cdr.maxPages) { ++cdr.thisPage; populate(); } } function handlerEnd() { if (cdr.thisPage !== cdr.maxPages) { cdr.thisPage = cdr.maxPages; populate(); } } function handlerGoto() { const page = parseInt(document.querySelector('.cdr .controls .goto-page').value, 10); if (!isNaN(page) && page !== cdr.thisPage && page >= 1 && page <= cdr.maxPages) { cdr.thisPage = page; populate(); } } // apply all filters and redraw function handlerApply() { let filters = {}; // iterate over filters, uglify and apply document.querySelectorAll('.left-panel .filters .filter .value').forEach( e => { const id = "" + e.getAttribute('filter-id'); if (id) { filters[id] = uglifyThing(id, e.value); if (!filters[id] || filters[id].length === 0) { delete filters[id]; } } }); // iterate over callType filters, combine into bitmask filters.callTypes = 0; ['internal', 'outgoing', 'incoming'].forEach( e => { filters.callTypes = (filters.callTypes << 1) + (document.querySelector('.left-panel .call-type.call-type_' + e).classList.contains('active') ? 1 : 0); }); // do not specify callfilters if none or all are selected if (filters.callTypes === 0 || filters.callTypes === 7) { delete filters.callTypes; } // iterate over wildcards if (document.querySelector('.left-panel .filters .filter .wildcard_source').classList.contains('active')) { filters.wcSource = true; } if (document.querySelector('.left-panel .filters .filter .wildcard_destination').classList.contains('active')) { filters.wcDestination = true; } if (document.querySelector('.left-panel .filters .filter .wildcard_any').classList.contains('active')) { filters.wcAny = true; } // compare new filters with old filters let shouldPopulate = false; for (let prop in filters) { if (filters[prop] !== cdr.filter[prop]) { shouldPopulate = true; break; } } // compare old filters with new filters for (let prop in cdr.filter) { if (typeof filters[prop] === 'undefined') { shouldPopulate = true; break; } } // should re-filter? if (shouldPopulate) { cdr.thisPage = 1; cdr.filter = filters; populate(); } } document.querySelector('.left-panel .filters .button.apply').addEventListener('click', wrapListener(handlerApply)); // reset all filters and redraw function handlerReset() { if (cdr.filter) { filterProperties.forEach(e => { delete cdr.filter[e]; }); } document.querySelectorAll('.left-panel .filters .filter .value').forEach( e => e.value = "" ); document.querySelectorAll('.left-panel .filters .filter .status').forEach( e => e.classList.remove('active') ); document.querySelectorAll('.left-panel .filters .call-type').forEach( e => e.classList.remove('active') ); populate(); } document.querySelector('.left-panel .filters .button.reset').addEventListener('click', wrapListener(handlerReset)); document.querySelector('.cdr .no-cdrs .button.reset').addEventListener('click', wrapListener(handlerReset)); // handle click on calltypes document.querySelectorAll('.left-panel .filters .call-types .call-type').forEach(e => e.addEventListener('click', wrapListener((ev, el) => { el.classList.toggle('active'); }, e))); // wildcard clicks document.querySelectorAll('.left-panel .filters .filter .wildcard').forEach(e => e.addEventListener('click', wrapListener((ev, el) => { el.classList.toggle('active'); }, e))); // handle click on settings and set initial values document.querySelectorAll('.left-panel .settings .checkbox').forEach(e => { const state = storageGet('setting-' + e.getAttribute('component-id')); if (state === 'on' || state === 'off') { e.querySelector('input').checked = (state === 'on'); } e.querySelector('input').addEventListener('click', wrapListener((ev, el) => { storageSet('setting-' + el.getAttribute('component-id'), el.querySelector('input').checked ? 'on' : 'off'); }, e)); }); // handle settings document.querySelector('.left-panel .settings .checkbox.grouping > input').addEventListener('click', populate); document.querySelector('.left-panel .settings .checkbox.time-in-seconds > input').addEventListener('click', draw); // handle page buttons document.querySelector('.cdr .controls .button_begin').addEventListener('click', wrapListener(handlerBegin)); document.querySelector('.cdr .controls .button_left').addEventListener('click', wrapListener(handlerLeft)); document.querySelector('.cdr .controls .button_right').addEventListener('click', wrapListener(handlerRight)); document.querySelector('.cdr .controls .button_end').addEventListener('click', wrapListener(handlerEnd)); document.querySelector('.cdr .controls .button_goto').addEventListener('click', wrapListener(handlerGoto)); // handle sorting document.querySelectorAll('.cdr .header .cell.sortable .cell-clickable-area').forEach( e => e.addEventListener('click', wrapListener(handlerSort, e))); createListener(document.querySelector('.left-panel .new-entries .button.update'), 'click', handlerDoUpdate); window.addEventListener('resize', ev => { let vh = window.innerHeight; let lph = Array.prototype.slice.call(document.querySelectorAll('.body-wrapper > .panel-wrapper > .left-panel > *')) .map(e => e.offsetHeight) .reduce((acc, cv) => acc + cv); let fh = document.querySelector('.body-wrapper > .footer').offsetHeight; let bh = 0; if (vh > lph + fh) { bh = vh - 165; } else { bh = lph + fh - 165; } document.querySelector('.cdr .table .body').style.maxHeight = bh + 'px'; }); document.onreadystatechange = e => { if (document.readyState === 'complete') { draw(); updateTimerHandle = setTimeout(performUpdate, 5000); } } // 3rd party stuff document.addEventListener('DOMContentLoaded', () => { OverlayScrollbars(document.querySelector('.cdr .body'), {}); }); const commonPikadayOptions = { format: 'dd.mm.yyyy', i18n: { previousMonth : 'Предыдущий месяц', nextMonth : 'Следующий месяц', months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], weekdays: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'], weekdaysShort: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] }, toString(date, format) { return date.toLocaleDateString('ru-ru', {}); }, theme: 'pd-theme', firstDay: 1 }; const startPicker = new Pikaday({ field: document.querySelector('.filters .filter_date-start .value'), ...commonPikadayOptions}); const endPicker = new Pikaday({ field: document.querySelector('.filters .filter_date-end .value'), ...commonPikadayOptions});