<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LeadDesk Dashboard</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f3; color: #1a1a18; min-height: 100vh; }
input, button { font-family: inherit; font-size: 14px; outline: none; }
input { width: 100%; padding: 8px 12px; border: 1px solid #d3d1c7; border-radius: 8px; background: #fff; color: #1a1a18; }
input:focus { border-color: #888780; }
button { cursor: pointer; padding: 8px 16px; border: 1px solid #d3d1c7; border-radius: 8px; background: #fff; color: #1a1a18; transition: background 0.15s; }
button:hover { background: #f1efe8; }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: #1a1a18; color: #fff; border-color: #1a1a18; }
.btn-primary:hover { background: #333; }
/* Login */
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }
.login-card { background: #fff; border: 1px solid #d3d1c7; border-radius: 12px; padding: 2rem; width: 100%; max-width: 380px; }
.login-card h1 { font-size: 18px; font-weight: 500; margin-bottom: 4px; }
.login-card p { font-size: 13px; color: #888780; margin-bottom: 1.5rem; }
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 12px; color: #5f5e5a; margin-bottom: 4px; }
.error-msg { font-size: 12px; color: #a32d2d; margin: 8px 0; }
.hint { font-size: 11px; color: #b4b2a9; margin-top: 1rem; }
/* Layout */
.app { display: flex; min-height: 100vh; }
.sidebar { width: 220px; background: #fff; border-right: 1px solid #d3d1c7; padding: 1.5rem 1rem; flex-shrink: 0; }
.sidebar h2 { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.sidebar p { font-size: 11px; color: #888780; margin-bottom: 1.5rem; }
.nav-item { display: block; width: 100%; text-align: left; padding: 7px 10px; border: none; border-radius: 6px; background: none; font-size: 13px; color: #5f5e5a; cursor: pointer; margin-bottom: 2px; }
.nav-item:hover { background: #f1efe8; color: #1a1a18; }
.nav-item.active { background: #f1efe8; color: #1a1a18; font-weight: 500; }
.disconnect { margin-top: auto; font-size: 12px; color: #888780; border: none; background: none; cursor: pointer; padding: 4px 0; }
.disconnect:hover { color: #a32d2d; }
.main { flex: 1; padding: 2rem; overflow-y: auto; }
.page-title { font-size: 18px; font-weight: 500; margin-bottom: 4px; }
.page-sub { font-size: 12px; color: #888780; margin-bottom: 1.5rem; }
/* Metrics */
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 1.5rem; }
.metric { background: #f1efe8; border-radius: 8px; padding: 1rem; }
.metric-label { font-size: 11px; color: #888780; margin-bottom: 4px; }
.metric-value { font-size: 22px; font-weight: 500; }
.metric-sub { font-size: 11px; color: #b4b2a9; margin-top: 4px; }
/* Cards */
.card { background: #fff; border: 1px solid #d3d1c7; border-radius: 12px; padding: 1rem; margin-bottom: 12px; }
.card-title { font-size: 12px; font-weight: 500; color: #5f5e5a; margin-bottom: 12px; }
/* Bar chart */
.bar-chart { display: flex; align-items: flex-end; gap: 4px; height: 80px; }
.bar-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.bar { width: 100%; background: #378add; border-radius: 3px 3px 0 0; min-height: 2px; }
.bar-lbl { font-size: 9px; color: #b4b2a9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; text-align: center; }
/* Mini bar */
.mini-bar-row { margin-bottom: 8px; }
.mini-bar-top { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 3px; }
.mini-bar-track { height: 6px; border-radius: 3px; background: #f1efe8; overflow: hidden; }
.mini-bar-fill { height: 100%; border-radius: 3px; background: #378add; }
/* Table */
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { padding: 9px 12px; text-align: left; font-weight: 500; color: #888780; font-size: 11px; border-bottom: 1px solid #d3d1c7; }
td { padding: 9px 12px; border-bottom: 1px solid #f1efe8; color: #1a1a18; }
tr:last-child td { border-bottom: none; }
/* Badge */
.badge { font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 6px; text-transform: capitalize; display: inline-block; }
.badge-answered { background: #eaf3de; color: #3b6d11; }
.badge-missed { background: #fcebeb; color: #a32d2d; }
.badge-voicemail { background: #faeeda; color: #854f0b; }
.badge-abandoned { background: #fbeaf0; color: #993556; }
.badge-default { background: #f1efe8; color: #5f5e5a; }
/* Grid */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 700px) { .grid-2 { grid-template-columns: 1fr; } .sidebar { display: none; } }
/* Loading */
.loading { text-align: center; padding: 3rem; color: #888780; font-size: 14px; }
</style>
</head>
<body>
<div id="root"></div>
<script>
const PROXY = "https://rough-frost-3472.olfr.workers.dev";
let token = null;
let dashData = {};
let currentTab = "overview";
function fmt(n) { return typeof n === "number" ? n.toLocaleString() : n ?? "—"; }
function pct(n) { return typeof n === "number" ? n.toFixed(1) + "%" : "—"; }
function dur(s) { if (!s) return "0s"; const m = Math.floor(s/60), sec = s%60; return m > 0 ? `${m}m ${sec}s` : `${sec}s`; }
function badgeClass(s) {
const map = { answered: "answered", missed: "missed", voicemail: "voicemail", abandoned: "abandoned" };
return "badge badge-" + (map[(s||"").toLowerCase()] || "default");
}
function apiFetch(path, opts = {}) {
return fetch(PROXY + path, {
...opts,
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${token}`, ...(opts.headers || {}) }
}).then(r => r.json());
}
async function doLogin() {
const clientId = document.getElementById("clientId").value.trim();
const clientSecret = document.getElementById("clientSecret").value.trim();
const customerId = document.getElementById("customerId").value.trim();
const errEl = document.getElementById("loginError");
errEl.textContent = "";
if (!clientId || !clientSecret || !customerId) { errEl.textContent = "Please fill in all fields."; return; }
document.getElementById("loginBtn").disabled = true;
document.getElementById("loginBtn").textContent = "Connecting…";
try {
const res = await fetch(PROXY + "/oauth/access-token", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ grant_type: "leaddesk_client_id", client_id: clientId, client_secret: clientSecret, leaddesk_client_id: parseInt(customerId) })
});
const j = await res.json();
if (!res.ok) throw new Error(j.error_description || j.error || "Authentication failed");
token = j.access_token;
renderLoading();
await fetchAll();
} catch(e) {
errEl.textContent = e.message;
document.getElementById("loginBtn").disabled = false;
document.getElementById("loginBtn").textContent = "Connect to LeadDesk";
}
}
async function fetchAll() {
try {
const [callsRes, agentsRes] = await Promise.allSettled([
apiFetch("/calls?per_page=100"),
apiFetch("/users?per_page=100")
]);
const calls = callsRes.status === "fulfilled" ? (callsRes.value?.collection || callsRes.value?.data || []) : [];
const agents = agentsRes.status === "fulfilled" ? (agentsRes.value?.collection || agentsRes.value?.data || []) : [];
const totalCalls = calls.length;
const answered = calls.filter(c => c.status?.toLowerCase() === "answered" || c.answered).length;
const missed = calls.filter(c => c.status?.toLowerCase() === "missed" || c.missed).length;
const avgDuration = totalCalls > 0 ? Math.round(calls.reduce((a, c) => a + (c.duration || 0), 0) / totalCalls) : 0;
const answerRate = totalCalls > 0 ? (answered / totalCalls) * 100 : 0;
const byStatus = {};
calls.forEach(c => { const s = c.status || "unknown"; byStatus[s] = (byStatus[s] || 0) + 1; });
const byHour = Array.from({ length: 24 }, (_, h) => ({
label: h % 4 === 0 ? `${h}:00` : "",
value: calls.filter(c => new Date(c.created_at || c.start_time || 0).getHours() === h).length
}));
const byAgent = {};
calls.forEach(c => {
const id = c.user_id || c.agent_id || "unknown";
if (!byAgent[id]) byAgent[id] = { answered: 0, total: 0, duration: 0 };
byAgent[id].total++;
if (c.status?.toLowerCase() === "answered" || c.answered) byAgent[id].answered++;
byAgent[id].duration += c.duration || 0;
});
const agentMap = {};
agents.forEach(a => { agentMap[a.id] = a.username || a.name || `Agent ${a.id}`; });
const agentStats = Object.entries(byAgent).map(([id, s]) => ({
name: agentMap[id] || `Agent ${id}`,
total: s.total, answered: s.answered,
rate: s.total > 0 ? ((s.answered / s.total) * 100).toFixed(1) : "0",
avgDur: s.total > 0 ? Math.round(s.duration / s.total) : 0
})).sort((a, b) => b.total - a.total).slice(0, 10);
dashData = { calls, agents, totalCalls, answered, missed, avgDuration, answerRate, byStatus, byHour, agentStats, totalAgents: agents.length };
renderDashboard();
} catch(e) {
renderError(e.message);
}
}
function renderLogin() {
document.getElementById("root").innerHTML = `
<div class="login-wrap">
<div class="login-card">
<h1>LeadDesk Dashboard</h1>
<p>Enter your API credentials to connect</p>
<div class="field"><label>API key (client ID)</label><input id="clientId" type="text" placeholder="your-api-key"></div>
<div class="field"><label>Client secret</label><input id="clientSecret" type="password" placeholder="your-client-secret"></div>
<div class="field"><label>LeadDesk customer ID</label><input id="customerId" type="text" placeholder="e.g. 12345"></div>
<div id="loginError" class="error-msg"></div>
<button id="loginBtn" class="btn-primary" style="width:100%;margin-top:4px" onclick="doLogin()">Connect to LeadDesk</button>
<p class="hint">Credentials are used only to fetch an OAuth token and are never stored.</p>
</div>
</div>`;
}
function renderLoading() {
document.getElementById("root").innerHTML = `<div class="loading">Connecting to LeadDesk…</div>`;
}
function renderError(msg) {
document.getElementById("root").innerHTML = `
<div class="login-wrap">
<div class="login-card">
<p class="error-msg">${msg}</p>
<button class="btn-primary" onclick="renderLogin()">Back to login</button>
</div>
</div>`;
}
function setTab(t) { currentTab = t; renderDashboard(); }
function renderDashboard() {
const { totalCalls, answered, missed, avgDuration, answerRate, byStatus, byHour, agentStats, calls, totalAgents } = dashData;
const maxStatus = Math.max(...Object.values(byStatus || {}), 1);
const maxBar = Math.max(...(byHour || []).map(d => d.value), 1);
const statusRows = Object.entries(byStatus || {}).sort((a,b) => b[1]-a[1]).map(([s, v]) => `
<div class="mini-bar-row">
<div class="mini-bar-top"><span style="text-transform:capitalize">${s}</span><span style="color:#888780">${v}</span></div>
<div class="mini-bar-track"><div class="mini-bar-fill" style="width:${Math.round((v/maxStatus)*100)}%"></div></div>
</div>`).join("") || `<p style="font-size:12px;color:#b4b2a9">No data</p>`;
const barCols = (byHour || []).map(d => `
<div class="bar-col">
<div class="bar" style="height:${Math.round((d.value/maxBar)*68)||2}px"></div>
<span class="bar-lbl">${d.label}</span>
</div>`).join("");
const agentRows = agentStats?.length > 0 ? agentStats.map(a => `
<tr>
<td>${a.name}</td><td>${a.total}</td><td>${a.answered}</td><td>${a.rate}%</td><td>${dur(a.avgDur)}</td>
</tr>`).join("") : `<tr><td colspan="5" style="text-align:center;color:#b4b2a9;padding:1.5rem">No agent data</td></tr>`;
const recentRows = calls?.slice(0, 25).map(c => {
const d = c.created_at || c.start_time ? new Date(c.created_at || c.start_time) : null;
return `<tr>
<td style="color:#888780">${d ? d.toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"}) : "—"}</td>
<td style="text-transform:capitalize">${c.direction || c.type || "—"}</td>
<td><span class="${badgeClass(c.status)}">${c.status || "unknown"}</span></td>
<td>${dur(c.duration)}</td>
<td style="font-family:monospace;color:#888780">${c.phone_number || c.callee || c.caller || "—"}</td>
</tr>`;
}).join("") || `<tr><td colspan="5" style="text-align:center;color:#b4b2a9;padding:1.5rem">No call records</td></tr>`;
const tabs = ["overview","agents","recent"].map(t => `
<button onclick="setTab('${t}')" style="background:none;border:none;border-bottom:${currentTab===t?"2px solid #1a1a18":"2px solid transparent"};border-radius:0;padding:6px 4px;font-size:13px;font-weight:${currentTab===t?"500":"400"};color:${currentTab===t?"#1a1a18":"#888780"};cursor:pointer;margin-right:12px">
${t.charAt(0).toUpperCase()+t.slice(1)}
</button>`).join("");
const overviewTab = `
<div class="grid-2">
<div class="card">
<div class="card-title">Calls by status</div>
${statusRows}
</div>
<div class="card">
<div class="card-title">Calls by hour</div>
<div class="bar-chart">${barCols}</div>
</div>
</div>`;
const agentsTab = `
<div class="card" style="padding:0;overflow:hidden">
<div class="tbl-wrap">
<table>
<thead><tr><th>Agent</th><th>Total</th><th>Answered</th><th>Answer rate</th><th>Avg duration</th></tr></thead>
<tbody>${agentRows}</tbody>
</table>
</div>
</div>`;
const recentTab = `
<div class="card" style="padding:0;overflow:hidden">
<div class="tbl-wrap">
<table>
<thead><tr><th>Time</th><th>Direction</th><th>Status</th><th>Duration</th><th>Number</th></tr></thead>
<tbody>${recentRows}</tbody>
</table>
</div>
</div>`;
document.getElementById("root").innerHTML = `
<div class="app">
<div class="sidebar">
<h2>LeadDesk</h2>
<p>Calls & activity</p>
<button class="nav-item active">Overview</button>
<div style="margin-top:auto;padding-top:2rem">
<button class="disconnect" onclick="token=null;renderLogin()">Disconnect</button>
</div>
</div>
<div class="main">
<div class="page-title">Calls & activity</div>
<div class="page-sub">Live data · LeadDesk CEU</div>
<div class="metrics">
<div class="metric"><div class="metric-label">Total calls</div><div class="metric-value">${fmt(totalCalls)}</div></div>
<div class="metric"><div class="metric-label">Answered</div><div class="metric-value">${fmt(answered)}</div><div class="metric-sub">${pct(answerRate)} answer rate</div></div>
<div class="metric"><div class="metric-label">Missed</div><div class="metric-value">${fmt(missed)}</div></div>
<div class="metric"><div class="metric-label">Avg duration</div><div class="metric-value">${dur(avgDuration)}</div></div>
<div class="metric"><div class="metric-label">Agents</div><div class="metric-value">${fmt(totalAgents)}</div></div>
</div>
<div style="display:flex;gap:8px;margin-bottom:1.25rem;border-bottom:1px solid #d3d1c7;padding-bottom:0">${tabs}</div>
${currentTab === "overview" ? overviewTab : currentTab === "agents" ? agentsTab : recentTab}
</div>
</div>`;
}
renderLogin();
</script>
</body>
</html>