// ============================================================================ // public/screens/dashboard.jsx — Real dashboard wired to /api/dashboard/summary // ---------------------------------------------------------------------------- // Fetches all widgets in one round-trip via /summary. Shows a skeleton state // while loading, then renders KPIs / call-volume / intent donut / hot leads / // active campaigns / today's schedule. Each widget knows how to render an // empty state when its data is missing — important for fresh tenants where // none of these will have rows yet. // ============================================================================ function DashboardScreen({ go, session }) { const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let alive = true; setLoading(true); setError(null); VoaisAPI.get("/api/dashboard/summary").then((r) => { if (!alive) return; if (r.ok && r.data?.ok) { setData(r.data); } else { setError(r.data?.msg || "Couldn't load dashboard data."); } setLoading(false); }); return () => { alive = false; }; }, []); if (loading) return ; if (error) return ; 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 */}
AI Summary
{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
{hotLeads.length === 0 ? (
) : ( hotLeads.map((l) => (
{l.contact_name || "Unknown"}
{l.interest || "—"} · {l.campaign_name || "—"}
{l.lead_score}
{timeAgo(l.started_at)}
)) )}
{/* ── Active campaigns table ── */}

Active campaigns

{campaigns.length > 0 && {campaigns.length} running}
}>Filter } onClick={() => go("campaigns")}>New campaign
{campaigns.length === 0 ? (
) : ( {campaigns.map((c) => { const pct = c.contacts > 0 ? Math.round((c.dialed / c.contacts) * 100) : 0; return ( go("campaigns")}> ); })}
Campaign Agent Progress Connected Hot leads Status
{c.name}
{c.industry || "—"} · {c.code}
{c.agent_name || "—"}
{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 (
{label}
{kpi.spark && }
{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 (
{[0,1,2,3].map((i) =>
)}
); } 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;