// 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 }; }