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.
297 lines
8.8 KiB
297 lines
8.8 KiB
2 years ago
|
'use strict';
|
||
|
|
||
|
const Joi = require('joi');
|
||
|
const Dayjs = require('dayjs');
|
||
|
const Rfr = require('rfr');
|
||
|
Dayjs.extend(require('dayjs/plugin/customParseFormat'));
|
||
|
|
||
|
const Common = Rfr('/shared/common');
|
||
|
const ComSrv = Rfr('/server/comsrv');
|
||
|
const Db = Rfr('/server/db');
|
||
|
|
||
|
const typeTable = {
|
||
|
incoming: Common.ctIncoming,
|
||
|
outgoing: Common.ctOutgoing,
|
||
|
internal: Common.ctInternal
|
||
|
};
|
||
|
|
||
|
const resTable = { // not used now
|
||
|
answered: Common.resAnswered,
|
||
|
unanswered: Common.resNoAnswer,
|
||
|
busy: Common.resBusy,
|
||
|
failed: Common.resFailed,
|
||
|
other: Common.resOther,
|
||
|
cancelled: Common.resCancelled
|
||
|
};
|
||
|
|
||
|
const transformDate = (date, clamp = '') => {
|
||
|
if (date) {
|
||
|
date = Dayjs(date, 'YYYY-MM-DD');
|
||
|
if (!date.isValid()) {
|
||
|
throw new Error('Invalid date specified');
|
||
|
}
|
||
|
|
||
|
if (clamp === 'start') {
|
||
|
date = date.startOf('day');
|
||
|
} else if (clamp === 'end') {
|
||
|
date = date.endOf('day')
|
||
|
}
|
||
|
|
||
|
return date;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const sortableFields = ['source', 'destination', 'callDate', 'totalDuration', 'callDuration', 'callType', 'callResult'];
|
||
|
const selectableFields = ['id', 'calldate', 'calltype', 'callresult', 'source', 'destination', 'callduration', 'totalduration', 'dailyid', 'dailyct', 'uniqueid', 'comment', 'ivr'];
|
||
|
|
||
|
const getFilter = async filter => {
|
||
|
const schema = Joi.object({
|
||
|
source: Joi.string().pattern(/^\d+$/).max(128),
|
||
|
destination: Joi.link('source'),
|
||
|
any: Joi.link('source'),
|
||
|
callTypes: Joi.number().min(0).max(Common.ctMaxSet),
|
||
|
dateStart: Joi.string().pattern(/^[0-9]{1,}-[0-9]{1,2}-[0-9]{1,2}$/).max(64),
|
||
|
dateEnd: Joi.link('dateStart'),
|
||
|
id: Joi.string().max(100),
|
||
|
|
||
|
wcSource: Joi.boolean().default(false),
|
||
|
wcDestination: Joi.boolean().default(false),
|
||
|
wcAny: Joi.boolean().default(false),
|
||
|
|
||
|
limit: Joi.number().integer().min(1).max(100).default(15),
|
||
|
page: Joi.number().integer().min(1).default(1),
|
||
|
sort: Joi.string().valid(...sortableFields).default('callDate'),
|
||
|
dir: Joi.string().valid('asc', 'desc').default('desc'),
|
||
|
|
||
|
disableGrouping: Joi.number().min(0).max(1).default(0),
|
||
|
})
|
||
|
|
||
|
let res = {
|
||
|
hasUserFilters: (!!filter.dateStart || !!filter.dateEnd || !!filter.callTypes ||
|
||
|
!!filter.id || !!filter.source || !!filter.destination || !!filter.any ||
|
||
|
!!filter.wcSource || !!filter.wcDestination || !!filter.wcAny),
|
||
|
...(await schema.validateAsync(filter))
|
||
|
};
|
||
|
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
const filterQueryBuilder = (qb, parms) => {
|
||
|
if (parms.source) {
|
||
|
if (parms.wcSource) { qb.where('source', 'like', '%' + parms.source + '%'); }
|
||
|
else { qb.where('source', '=', parms.source); }
|
||
|
}
|
||
|
if (parms.destination) {
|
||
|
if (parms.wcDestination) { qb.where('destination', 'like', '%' + parms.destination + '%'); }
|
||
|
else { qb.where('destination', '=', parms.destination); }
|
||
|
}
|
||
|
if (parms.any) {
|
||
|
if (parms.wcAny) { qb.where(function() { this.where('source', 'like', '%' + parms.any + '%').orWhere('destination', 'like', '%' + parms.any + '%') }); }
|
||
|
else { qb.where(function() { this.where('source', '=', parms.any).orWhere('destination', '=', parms.any) }); }
|
||
|
}
|
||
|
|
||
|
if (parms.dateStart) { qb.where('calldate', '>=', transformDate(parms.dateStart, 'start')); }
|
||
|
if (parms.dateEnd) { qb.where('calldate', '<=', transformDate(parms.dateEnd, 'end')); }
|
||
|
if (parms.id) { qb.where('dailyct', parms.id); }
|
||
|
|
||
|
if (parms.callTypes) {
|
||
|
const a = [];
|
||
|
[Common.ctIncoming, Common.ctOutgoing, Common.ctInternal].forEach( e => {
|
||
|
if (parms.callTypes & (1 << e)) { a.push(e); }
|
||
|
});
|
||
|
if (a.length > 0) { qb.whereIn('calltype', a); }
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const groupQueryBuilder = (qb, parms) => {
|
||
|
// special case: if grouping and sorting is enabled, also sort by date (desc)
|
||
|
if (!parms.disableGrouping && parms.sort !== 'callDate') {
|
||
|
qb.orderBy('calldate', 'desc');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const getRawRecords = async (parms) => {
|
||
|
const rec = await Db.query('ptable')
|
||
|
.select(...selectableFields)
|
||
|
.modify(filterQueryBuilder, parms)
|
||
|
.offset((parms.page - 1) * parms.limit)
|
||
|
.limit(parms.limit)
|
||
|
.orderBy(parms.sort.toLowerCase(), parms.dir.toLowerCase())
|
||
|
.modify(groupQueryBuilder, parms);
|
||
|
|
||
|
return Common.toArray(rec);
|
||
|
}
|
||
|
|
||
|
const mapRawRecords = rec => {
|
||
|
const f = (r => { return {
|
||
|
callDate: Dayjs(r.calldate),
|
||
|
callType: r.calltype,
|
||
|
callResult: r.callresult,
|
||
|
source: r.source,
|
||
|
destination: r.destination,
|
||
|
callDuration: r.callduration,
|
||
|
totalDuration: r.totalduration,
|
||
|
dailyID: r.dailyid,
|
||
|
dailyCT: r.dailyct,
|
||
|
uniqueID: r.uniqueid,
|
||
|
comment: (r.comment || ''),
|
||
|
ivr: (r.ivr || 0)
|
||
|
}});
|
||
|
|
||
|
return rec.map(f);
|
||
|
};
|
||
|
|
||
|
|
||
|
const countSinceLastUpdate = async lastUpdateAt => {
|
||
|
const res = await Db.query('ptable')
|
||
|
.where('calldate', '>', lastUpdateAt)
|
||
|
.count({num: 'calldate'});
|
||
|
|
||
|
return res[0].num;
|
||
|
};
|
||
|
|
||
|
const getLatest = async () => {
|
||
|
const res = Common.toArray(await Db.query('ptable')
|
||
|
.select('calldate')
|
||
|
.orderBy('calldate', 'desc')
|
||
|
.first()
|
||
|
);
|
||
|
|
||
|
return res[0].calldate;
|
||
|
};
|
||
|
|
||
|
const countRecords = async (parms) => {
|
||
|
const res = await Db.query('ptable')
|
||
|
.modify(filterQueryBuilder, parms)
|
||
|
.count({num: 'uniqueid'}).first();
|
||
|
|
||
|
return res.num;
|
||
|
};
|
||
|
|
||
|
// Check if 1st record extends "0th" record by date
|
||
|
const isFirstCont = async (parms, mapped) => {
|
||
|
const first = mapped[0];
|
||
|
if (first) {
|
||
|
const res = await Db.query('ptable')
|
||
|
.select(...selectableFields)
|
||
|
.modify(filterQueryBuilder, parms)
|
||
|
.modify( (qb, parms) => {
|
||
|
if (parms.sort === 'callDate' && parms.dir === 'asc') {
|
||
|
qb.where('calldate', '<', first.callDate)
|
||
|
.andWhere('calldate', '>=', Dayjs(first.callDate).startOf('day'));
|
||
|
} else {
|
||
|
qb.where('calldate', '>', first.callDate)
|
||
|
.andWhere('calldate', '<=', Dayjs(first.callDate).endOf('day'));
|
||
|
}
|
||
|
|
||
|
if (parms.sort !== 'callDate') {
|
||
|
qb.where(parms.sort.toLowerCase(), '=', first[parms.sort]);
|
||
|
}
|
||
|
}, parms);
|
||
|
|
||
|
if (res.length >= 1) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
const getDailyStats = async (parms, mapped) => {
|
||
|
const allDays = mapped.map( r => Dayjs(r.callDate).startOf('day').valueOf() )
|
||
|
.filter( (e, i, a) => a.indexOf(e) === i);
|
||
|
|
||
|
let result = [];
|
||
|
|
||
|
for (const e of allDays) {
|
||
|
const res = await Db.query('ptable')
|
||
|
.sum({ total_duration_sum: 'totalduration', call_duration_sum: 'callduration' })
|
||
|
.count({ total_calls: 'uniqueid' })
|
||
|
.modify(filterQueryBuilder, parms)
|
||
|
.modify( (qb, parms) => {
|
||
|
qb.where('calldate', '>=', Dayjs(e).startOf('day'))
|
||
|
.andWhere('calldate', '<=', Dayjs(e).endOf('day'));
|
||
|
}, parms);
|
||
|
|
||
|
result.push({
|
||
|
day: e,
|
||
|
totalDuration: res[0].total_duration_sum,
|
||
|
callDuration: res[0].call_duration_sum,
|
||
|
totalCalls: res[0].total_calls
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
const getCdr = async (filter) => {
|
||
|
const count = await countRecords(filter);
|
||
|
const raw = await getRawRecords(filter);
|
||
|
const mapped = mapRawRecords(raw);
|
||
|
|
||
|
return {
|
||
|
dailyStats: (await getDailyStats(filter, mapped)),
|
||
|
isFirstCont: (!filter.disableGrouping && (await isFirstCont(filter, mapped))),
|
||
|
maxPages: (filter.limit > 0 && count > 0) ? Math.ceil(count / filter.limit) : 1,
|
||
|
thisPage: filter.page,
|
||
|
records: mapped,
|
||
|
filter: filter,
|
||
|
latest: (await getLatest())
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
const getCdrById = async (id) => {
|
||
|
const raw = Common.toArray(await Db.query('ptable')
|
||
|
.select(...selectableFields)
|
||
|
.where('uniqueid', id)
|
||
|
.first()
|
||
|
);
|
||
|
|
||
|
const mapped = mapRawRecords(raw);
|
||
|
|
||
|
return {
|
||
|
...mapped[0],
|
||
|
date: Dayjs(mapped[0].callDate).format('YYYY-MM-DD_HH-mm-ss'),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
|
||
|
const queryHandler = async (request, h) => {
|
||
|
return await getCdr(await getFilter(request.query));
|
||
|
};
|
||
|
|
||
|
const checkNewHandler = async(request, h) => {
|
||
|
return {
|
||
|
numSinceLastUpdate: await countSinceLastUpdate(Dayjs(request.query.lastUpdateAt))
|
||
|
};
|
||
|
};
|
||
|
|
||
|
|
||
|
module.exports = {
|
||
|
getCdr: getCdr,
|
||
|
getFilter: getFilter,
|
||
|
getCdrById: getCdrById,
|
||
|
|
||
|
routes: [{
|
||
|
method: 'GET',
|
||
|
path: '/api/query',
|
||
|
handler: queryHandler
|
||
|
},
|
||
|
{
|
||
|
method: 'GET',
|
||
|
path: '/api/check-new',
|
||
|
handler: checkNewHandler,
|
||
|
options: {
|
||
|
validate: {
|
||
|
query: Joi.object({
|
||
|
lastUpdateAt: Joi.date().timestamp('unix').required()
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}]
|
||
|
};
|