// ============================================================================ // public/screens/call-history.jsx — Call History (Phase 5) // ---------------------------------------------------------------------------- // Paginated table with filters + call detail side panel with transcript. // ============================================================================ function CallHistoryScreen() { const [calls, setCalls] = React.useState(null); const [pagination, setPag] = React.useState({ page: 1, limit: 25, total: 0, pages: 0 }); const [stats, setStats] = React.useState(null); const [filters, setFilters] = React.useState({ search: "", status: "", outcome: "", interest: "", quality: "" }); const [detail, setDetail] = React.useState(null); // full call detail object const [detailLoading, setDL] = React.useState(false); const load = React.useCallback(async (page = 1) => { const params = new URLSearchParams(); params.set("page", page); params.set("limit", 25); if (filters.search) params.set("search", filters.search); if (filters.status) params.set("status", filters.status); if (filters.outcome) params.set("outcome", filters.outcome); if (filters.interest) params.set("interest", filters.interest); if (filters.quality) params.set("quality", filters.quality); const r = await VoaisAPI.get("/api/calls?" + params.toString()); if (r.ok && r.data?.ok) { setCalls(r.data.calls); setPag(r.data.pagination); } }, [filters]); const loadStats = React.useCallback(async () => { const r = await VoaisAPI.get("/api/calls/stats"); if (r.ok && r.data?.ok) setStats(r.data.stats); }, []); React.useEffect(() => { load(1); loadStats(); }, [load, loadStats]); // Open call detail const openDetail = async (callId) => { setDL(true); const r = await VoaisAPI.get("/api/calls/" + callId); if (r.ok && r.data?.ok) { setDetail({ call: r.data.call, transcript: r.data.transcript, events: r.data.events }); } setDL(false); }; // Retry const handleRetry = async (callId) => { if (!confirm("Retry this call?")) return; const r = await VoaisAPI.post("/api/calls/" + callId + "/retry"); if (r.ok) { alert("Retry queued."); load(pagination.page); } else alert(r.data?.msg || "Retry failed."); }; // Export const handleExport = () => { const base = (window.VOAIS_API_BASE || ""); const sess = VoaisAPI.getSession(); window.open(base + "/api/calls/export?token=" + (sess?.token || ""), "_blank"); }; const updateFilter = (key, val) => setFilters(prev => ({ ...prev, [key]: val })); if (calls === null) return ; return (
{/* KPI cards */} {stats && (
)} {/* Filters row */}
updateFilter("search", e.target.value)} onKeyDown={e => { if (e.key === "Enter") load(1); }} style={{ paddingLeft: 32 }}/>
} onClick={() => load(1)}>Search } onClick={handleExport}>Export CSV
{/* Table */}
{calls.map(c => { const statusTone = c.status === "completed" ? "green" : c.status === "failed" ? "red" : c.status === "in_progress" ? "blue" : "gray"; const qualTone = c.lead_quality === "hot" ? "red" : c.lead_quality === "warm" ? "yellow" : "gray"; return ( openDetail(c.id)} style={{ cursor: "pointer" }}> ); })} {calls.length === 0 && ( )}
Call Number Agent Status Outcome Interest Score Duration When
{c.code || "—"}
{c.direction}
{c.number || "—"}
{c.agent_gradient &&
} {c.agent_name || "—"}
{c.status} {c.outcome || "—"} {c.interest ? {c.interest} : "—"} {c.lead_score != null ? ( = 80 ? "var(--ok)" : c.lead_score >= 50 ? "var(--warn)" : "var(--ink-3)" }}> {c.lead_score} ) : "—"} {c.duration_s ? formatDuration(c.duration_s) : "—"} {timeAgo(c.started_at)}
No calls found.
{/* Pagination */} {pagination.pages > 1 && (
load(pagination.page - 1)}>Prev Page {pagination.page} of {pagination.pages} ({pagination.total} calls) = pagination.pages} onClick={() => load(pagination.page + 1)}>Next
)}
{/* Call Detail Drawer */} {detail && ( setDetail(null)} onRetry={() => handleRetry(detail.call.id)}/> )}
); } // ── KPI mini card ─────────────────────────────────────────────────────── function KpiMini({ label, value, color }) { return (
{label}
{value}
); } // ── Call Detail Drawer ────────────────────────────────────────────────── function CallDetailDrawer({ detail, loading, onClose, onRetry }) { const { call, transcript, events } = detail; const [tab, setTab] = React.useState("transcript"); return (
{/* Call summary header */}
Number
{call.number || "—"}
Agent
{call.agent_name || "—"}
Duration
{call.duration_s ? formatDuration(call.duration_s) : "—"}
{/* Badges row */}
{call.status} {call.outcome && {call.outcome}} {call.interest && {call.interest}} {call.lead_quality && {call.lead_quality}} {call.lead_score != null && Score: {call.lead_score}} {call.sentiment && {call.sentiment}}
{/* Summary */} {call.summary && (
{call.summary}
)} {/* Tabs */} {/* Transcript */} {tab === "transcript" && (
{transcript.length === 0 && (
No transcript available.
)} {transcript.map((t, i) => (
{t.role === "agent" ? (call.agent_name || "AI") : "Caller"} {t.ts_ms != null && {formatDuration(Math.round(t.ts_ms / 1000))}} {t.intent_tag && {t.intent_tag}}
{t.content}
))}
)} {/* Events timeline */} {tab === "events" && (
{events.length === 0 && (
No events logged.
)} {events.map((ev, i) => (
{ev.event}
{ev.message &&
{ev.message}
}
{new Date(ev.created_at).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })}
))}
)} {/* Details tab */} {tab === "info" && (
Call UUID:{call.call_uuid} Direction:{call.direction} Mode:{call.mode} Language:{call.language} LLM:{call.llm_provider} / {call.llm_model || "—"} Campaign:{call.campaign_name || "—"} Contact:{call.contact_name || "—"} ({call.contact_phone || "—"}) Started:{call.started_at ? new Date(call.started_at).toLocaleString("en-IN") : "—"} Answered:{call.answered_at ? new Date(call.answered_at).toLocaleString("en-IN") : "—"} Ended:{call.ended_at ? new Date(call.ended_at).toLocaleString("en-IN") : "—"} Hangup cause:{call.hangup_cause || "—"} Cost:{call.cost_inr != null ? "₹" + Number(call.cost_inr).toFixed(2) : "—"} {call.recording_url && <> Recording:
)} {/* Actions */}
{(call.status === "failed" || call.status === "completed") && ( } onClick={onRetry}>Retry call )} Close
); } // ── Helpers ────────────────────────────────────────────────────────────── function formatDuration(sec) { if (!sec || sec <= 0) return "0s"; const m = Math.floor(sec / 60); const s = sec % 60; return m > 0 ? `${m}m ${s}s` : `${s}s`; } function timeAgo(dateStr) { if (!dateStr) return "—"; const d = new Date(dateStr); const now = Date.now(); const diff = Math.floor((now - d.getTime()) / 1000); if (diff < 60) return "just now"; if (diff < 3600) return Math.floor(diff / 60) + "m ago"; if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; if (diff < 604800) return Math.floor(diff / 86400) + "d ago"; return d.toLocaleDateString("en-IN", { day: "numeric", month: "short" }); } window.CallHistoryScreen = CallHistoryScreen;