'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() }) } } }] };