☎️ Web interface for viewing and processing Asterisk call logs (2020)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

635 lines
21 KiB

2 years ago
'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});