collabSupport/services/callReport/formatCallLine.js
jmcqueen a25fc08fe2 Rename /voicereport to /callreport with scoped user and phone targets.
Add store/email/phone filtering, richer call-line formatting, CDR feed pagination and queueing, and split Jira poller enrichment into testable modules.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:10:03 -04:00

224 lines
6.7 KiB
JavaScript

// services/callReport/formatCallLine.js
// Readable per-call lines for /callreport.
import { DateTime } from 'luxon';
export const WAN_THRESHOLDS = {
mosFair: 4,
mosUnusable: 3.5,
jitterMs: 40,
lossPct: 5,
};
export function formatDurationHuman(seconds) {
const s = Math.max(0, Math.round(Number(seconds) || 0));
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
const rem = s % 60;
return rem ? `${m}m ${rem}s` : `${m}m`;
}
export function formatLocalTime(iso, timeZone) {
if (!iso) return '—';
try {
return DateTime.fromISO(String(iso), { zone: 'utc' })
.setZone(timeZone || 'utc')
.toFormat('h:mma')
.toLowerCase();
} catch {
return String(iso);
}
}
function fmtPhone(num) {
if (!num) return null;
const s = String(num).trim();
if (!s) return null;
if (s.startsWith('+')) return s;
const d = s.replace(/\D/g, '');
if (d.length === 10) return `+1${d}`;
if (d.length === 11 && d.startsWith('1')) return `+${d}`;
return s;
}
function partyLabel(lineId, number) {
const phone = fmtPhone(number);
const lid = lineId && String(lineId).toUpperCase() !== 'NA' ? String(lineId).trim() : null;
if (lid && phone) return `${lid} (${phone})`;
if (lid) return lid;
if (phone) return phone;
return '?';
}
function storeUserLabel(user, number) {
const phone = fmtPhone(number);
const u = user && String(user).trim();
if (u && phone) return `${u} (${phone})`;
if (u) return u;
if (phone) return phone;
return '?';
}
function outboundOriginNumber(number) {
const numRaw = number && String(number).trim();
if (!numRaw) return null;
if (numRaw.length <= 5 && !numRaw.startsWith('+')) return numRaw;
return fmtPhone(number) || numRaw;
}
function formatOutboundOrigin(call) {
const user = call.endpointUser || call.leftParty?.user;
const number = call.leftParty?.number || call.callingNumber;
const model = call.model || call.endpointModel || call.leftParty?.model;
const u = user && String(user).trim();
const num = outboundOriginNumber(number);
const inner = [num, model && String(model).trim()].filter(Boolean).join(', ');
if (u && inner) return `${u} (${inner})`;
if (u) return u;
if (inner) return inner;
return '?';
}
function isShortDial(num) {
const s = String(num || '').trim();
if (!s) return false;
if (/^[#*]?\d{1,4}$/.test(s)) return true;
const d = s.replace(/\D/g, '');
return d.length > 0 && d.length <= 4;
}
function formatRemoteParty(call) {
const num = call.calledNumber || call.rightParty?.number;
if (call.direction === 'outbound' && isShortDial(num)) {
return `dialed ${num}`;
}
return fmtPhone(num) || '?';
}
function isAaOnly(call) {
return Boolean(call.reachedAttendant && !call.reachedPhone);
}
function rightSuffix(call) {
const aaOnly = isAaOnly(call);
const parts = [];
if (!aaOnly && call.finalNumber && call.finalNumber !== call.calledNumber && call.finalNumber !== call.storeMainNumber) {
parts.push(`final ${call.finalNumber}`);
}
if (!aaOnly && call.model) parts.push(call.model);
if (aaOnly && call.aaKeyPress) {
parts.push(`AA key ${call.aaKeyPress}`);
}
if (!parts.length) return '';
return ` (${parts.join(', ')})`;
}
function roundMetric(value, decimals = 1) {
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
const factor = 10 ** decimals;
return Math.round(value * factor) / factor;
}
function isCleanSuccess(call) {
if (call.abnormal || !call.normalOutcome) return false;
const disposition = String(call.disposition || call.outcome || '').trim();
return disposition.toLowerCase() === 'success';
}
/**
* Leading icon for a call line.
*/
export function formatCallPrefix(call) {
if (isAaOnly(call)) return '⚠️ ';
if (call.abnormal || !call.normalOutcome) return '⚠️ ';
if (isCleanSuccess(call)) return '✅ ';
return '';
}
/**
* Trailing disposition text (omits redundant "Success" when prefixed with ✅).
*/
export function formatDispositionSuffix(call) {
if (isAaOnly(call)) return '';
if (isCleanSuccess(call)) return '';
const disposition = call.disposition || call.outcome || 'unknown';
if (String(disposition).trim().toLowerCase() === 'success') return '';
return ` ${disposition}`;
}
function wanMetricLabel(label, value, unit, warnIcon, criticalIcon) {
let icon = '';
if (criticalIcon) icon = ` ${criticalIcon}`;
else if (warnIcon) icon = ` ${warnIcon}`;
return `${label}: ${value}${unit}${icon}`;
}
/**
* @param {object} wan
* @returns {string|null}
*/
export function formatWanLine(wan) {
if (!wan) return null;
const bits = [];
const mos = roundMetric(wan.mos, 1);
const jitter = roundMetric(wan.jitter, 1);
const loss = roundMetric(wan.loss, 1);
if (mos != null) {
let warn = null;
let critical = null;
if (mos < WAN_THRESHOLDS.mosUnusable) critical = '‼️';
else if (mos < WAN_THRESHOLDS.mosFair) warn = '⚠️';
bits.push(wanMetricLabel('MOS', mos, '', warn, critical));
}
if (jitter != null) {
const warn = jitter > WAN_THRESHOLDS.jitterMs ? '⚠️' : null;
bits.push(wanMetricLabel('Jitter', jitter, 'ms', warn, null));
}
if (loss != null) {
const warn = loss > WAN_THRESHOLDS.lossPct ? '⚠️' : null;
bits.push(wanMetricLabel('Loss', loss, '%', warn, null));
}
return bits.length ? bits.join(' ') : null;
}
/**
* @param {object} call grouped call from groupCdrCalls
* @param {{ timeZone?: string, includeWan?: boolean, detail?: boolean }} opts
* @returns {{ line1: string, line2: string|null }}
*/
export function formatCallBlock(call, opts = {}) {
const { timeZone, includeWan = true, detail = false } = opts;
const time = formatLocalTime(call.start, timeZone);
const dur = formatDurationHuman(call.duration);
const prefix = formatCallPrefix(call);
let left;
let right;
if (call.direction === 'inbound') {
left = partyLabel(call.callingLineId || call.leftParty?.lineId, call.callingNumber || call.leftParty?.number);
const main = fmtPhone(call.storeMainNumber || call.calledNumber || call.rightParty?.main) || '?';
right = `${main}${rightSuffix(call)}`;
} else if (call.direction === 'outbound') {
left = formatOutboundOrigin(call);
right = formatRemoteParty(call);
} else {
left = partyLabel(call.callingLineId, call.callingNumber);
right = fmtPhone(call.calledNumber) || '?';
}
const suffix = formatDispositionSuffix(call);
let line1 = `${prefix}${time} (${dur}): ${left}${right}${suffix}`;
if (detail && call.correlationId) {
line1 += ` _(${call.legCount} leg(s), ${call.correlationId.slice(0, 8)}…)_`;
}
let line2 = null;
if (includeWan && call.wan) {
const wanText = formatWanLine(call.wan);
if (wanText) line2 = ` ${wanText}`;
}
return { line1, line2 };
}