;
if (!data) return null;
const { kpis, callVolume, intent, hotLeads, campaigns, schedule } = data;
const isFresh = (kpis.callsToday.value === 0 && callVolume.total === 0);
return (
{/* Empty-state banner for brand-new tenants */}
{isFresh && (
Welcome to VoAIs Call!
Your dashboard is empty because you haven't run any campaigns yet. Set up your first AI agent and contact list to get started.
} onClick={() => go("agents")}>Create agent
} onClick={() => go("contacts")}>Import contacts
)}
{/* ── KPI strip ── */}
Number(v).toLocaleString("en-IN")} period="vs yesterday"/>
`${v}%`} period="vs yesterday"/>
Number(v).toLocaleString("en-IN")} period="vs yesterday"/>
Number(v).toLocaleString("en-IN")} period={`of ${(kpis.minutesUsed.ofTotal || 0).toLocaleString("en-IN")} included`}/>
{/* ── Call volume + Today's schedule ── */}
{/* Call volume — last 12 months */}
{}}/>
}
/>
{Number(callVolume.total).toLocaleString("en-IN")}
{callVolume.delta && callVolume.delta !== "—" && (
{callVolume.trend === "up" ? "▲" : callVolume.trend === "down" ? "▼" : "●"} {callVolume.delta}
)}
vs {Number(callVolume.priorTotal).toLocaleString("en-IN")} last period
{callVolume.total === 0
?
:
v >= 1000 ? Math.round(v / 1000) + "k" : v}/>
}
{/* Today's schedule */}
{new Date().toLocaleDateString("en-IN", { month: "short", day: "numeric", year: "numeric" })}
}
/>
{schedule.length === 0 ? (
) : (
{schedule.map((s, i) => {
const sched = parseJson(s.schedule_json) || {};
const colors = ["var(--viz-green)", "var(--accent)", "var(--viz-purple)", "var(--viz-orange)"];
return (
{s.name}
{sched.start || "—"} · {s.remaining} contacts pending
);
})}
)}
{/* ── AI summary + Intent donut + Hot leads ── */}
{/* AI Insights — static for now, Phase 7 makes it real */}
{campaigns.length > 0 ? (
<>Your top performer this week is {campaigns[0].name} with a {Math.round((campaigns[0].connected / Math.max(1, campaigns[0].dialed)) * 100)}% connect rate. The AI summary will get richer once you have more campaigns running.>
) : (
<>Once you have a few campaigns running, this card will surface actionable insights — best calling times, top objections, and which agents are converting.>
)}
Best calling time
{intent.total > 0 ? "11AM–1PM" : "—"}
0 ? "var(--ok)" : "var(--ink-3)", marginTop: 2 }}>{intent.total > 0 ? "+24% pickup" : "Need more data"}
Top objection
{intent.total > 0 ? "Too busy" : "—"}
{intent.total > 0 ? "32% of failed" : "Need more data"}
{/* Intent donut */}
{}}/>
}/>
{intent.total === 0 ? (
) : (
<>
{Number(intent.total).toLocaleString("en-IN")}
Total calls
}
/>
{intent.segments.map((x, i) => (
{x.label}
{x.value}%
))}
Hot leads share is{" "}
{intent.segments[0].value}%
of all calls.
>
)}
{/* Hot leads list */}
Hot Leads
{hotLeads.length} new
go("history")} title="View all">
{hotLeads.length === 0 ? (
) : (
hotLeads.map((l) => (
{l.contact_name || "Unknown"}
{l.interest || "—"} · {l.campaign_name || "—"}
{l.lead_score}
{timeAgo(l.started_at)}
))
)}
go("history")}>
View all leads
{/* ── Active campaigns table ── */}
Active campaigns
{campaigns.length > 0 &&
{campaigns.length} running }
}>Filter
} onClick={() => go("campaigns")}>New campaign
{campaigns.length === 0 ? (
) : (
Campaign
Agent
Progress
Connected
Hot leads
Status
{campaigns.map((c) => {
const pct = c.contacts > 0 ? Math.round((c.dialed / c.contacts) * 100) : 0;
return (
go("campaigns")}>
{c.name}
{c.industry || "—"} · {c.code}
{Number(c.dialed).toLocaleString("en-IN")} / {Number(c.contacts).toLocaleString("en-IN")}
{c.connect_rate ? `${Math.round(c.connect_rate)}%` : "—"}
{c.hot}
{c.status}
);
})}
)}
);
}
// ── KPI card ───────────────────────────────────────────────────────────
function KpiCard({ label, kpi, sparkColor, valueFormat, period }) {
if (!kpi) return null;
const trendSymbol = kpi.trend === "up" ? "▲" : kpi.trend === "down" ? "▼" : "●";
return (
{valueFormat ? valueFormat(kpi.value) : kpi.value}
{trendSymbol} {kpi.delta}
{period}
);
}
// ── Mini calendar — UI-only for now ────────────────────────────────────
function MiniCalendar() {
const now = new Date();
const today = now.getDate();
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
return (
{["M", "T", "W", "T", "F", "S", "S"].map((d, i) => (
{d}
))}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1;
const isToday = day === today;
const isPast = day < today;
return (
{day}
);
})}
);
}
// ── Skeleton (loading state) ───────────────────────────────────────────
function DashboardSkeleton() {
return (
);
}
function DashboardError({ msg }) {
return (
Couldn't load dashboard
{msg}
location.reload()} style={{ marginTop: 16 }}>Retry
);
}
// ── Utilities ──────────────────────────────────────────────────────────
function timeAgo(iso) {
if (!iso) return "—";
const d = new Date(iso);
const s = Math.max(1, Math.floor((Date.now() - d.getTime()) / 1000));
if (s < 60) return s + "s ago";
if (s < 3600) return Math.floor(s / 60) + " min ago";
if (s < 86400) return Math.floor(s / 3600) + " hr ago";
return Math.floor(s / 86400) + " d ago";
}
function parseJson(v) {
if (v === null || v === undefined) return null;
if (typeof v === "object") return v;
try { return JSON.parse(v); } catch (_) { return null; }
}
window.DashboardScreen = DashboardScreen;