// analytics.jsx — Doctor & clinic analytics wired to /api/analytics. const { useState: useASt, useMemo: useAMemo, useEffect: useAEf } = React; const PALETTE = [ "var(--primary)", "var(--accent-success)", "var(--accent-warn)", "oklch(0.65 0.10 290)", "var(--accent-danger)", "oklch(0.55 0.10 220)", "oklch(0.55 0.10 30)", ]; function DeltaBadge({ delta, invert }) { if (delta === null || delta === undefined || isNaN(delta)) return null; const positive = invert ? delta < 0 : delta > 0; const neutral = delta === 0; const arrow = delta > 0 ? "↑" : delta < 0 ? "↓" : "—"; const color = neutral ? "var(--ink-4)" : positive ? "oklch(0.40 0.13 155)" : "oklch(0.45 0.18 25)"; const bg = neutral ? "var(--bg-sunk)" : positive ? "color-mix(in oklch, var(--accent-success) 12%, white)" : "color-mix(in oklch, var(--accent-danger) 12%, white)"; return ( {arrow} {Math.abs(delta)}% ); } function BarChart({ data, color = "var(--primary)", height = 160 }) { const t = window.MSStore.useT(); const [hovered, setHovered] = useASt(null); const max = Math.max(...data.map(d => d.count), 1); const w = 520; const padX = 4; const barW = data.length ? Math.max(8, Math.floor((w - padX * 2) / data.length) - 6) : 0; if (!data.length) { return
{t("an.noData")}
; } return (
{[0.25, 0.5, 0.75, 1].map(pct => { const y = height - pct * height + 4; return ( {Math.round(pct * max)} ); })} {data.map((d, i) => { const x = padX + i * ((w - padX * 2) / data.length) + 3; const barH = d.count === 0 ? 0 : Math.max(4, (d.count / max) * (height - 8)); const y = height - barH + 4; const isHov = hovered === i; return ( setHovered(i)} onMouseLeave={() => setHovered(null)}> {isHov && d.count > 0 && ( {t("an.visitsCount", { n: d.count, plural: d.count === 1 ? "" : "s" })} )} {d.day} ); })}
); } function DonutChart({ items }) { const t = window.MSStore.useT(); const total = items.reduce((s, i) => s + i.count, 0); const r = 52, cx = 70, cy = 70, stroke = 22; const circ = 2 * Math.PI * r; if (total === 0) { return
{t("an.noData")}
; } let offset = 0; return (
{items.map((item, i) => { const pct = item.count / total; const dash = pct * circ; const gap = circ - dash; const el = ( ); offset += pct; return el; })} {total} {t("an.total")}
{items.map(item => (
{item.label} {item.count} {Math.round(item.count / total * 100)}%
))}
); } // Build the bar chart data given a range and the analytics payload's // `sessions_per_day` (each entry: {day: "YYYY-MM-DD", count}). function buildBars(range, sessionsPerDay, locale, t) { const byDay = {}; for (const r of (sessionsPerDay || [])) byDay[r.day] = r.count; const today = new Date(); today.setHours(0, 0, 0, 0); const out = []; if (range === "week") { for (let i = 6; i >= 0; i--) { const d = new Date(today.getTime() - i * 86400000); const key = d.toISOString().slice(0, 10); out.push({ day: d.toLocaleDateString(locale, { weekday: "short" }), count: byDay[key] || 0 }); } } else if (range === "month") { for (let w = 4; w >= 1; w--) { const start = new Date(today.getTime() - (w * 7 - 1) * 86400000); const end = new Date(start.getTime() + 6 * 86400000); let count = 0; for (let i = 0; i < 7; i++) { const d = new Date(start.getTime() + i * 86400000); count += byDay[d.toISOString().slice(0, 10)] || 0; } out.push({ day: t("an.weekShort", { n: 5 - w }), count }); } } else { // quarter for (let m = 2; m >= 0; m--) { const monthStart = new Date(today.getFullYear(), today.getMonth() - m, 1); const monthEnd = new Date(today.getFullYear(), today.getMonth() - m + 1, 1); let count = 0; for (const k in byDay) { const d = new Date(k); if (!isNaN(d) && d >= monthStart && d < monthEnd) count += byDay[k]; } out.push({ day: monthStart.toLocaleDateString(locale, { month: "short" }), count }); } } return out; } function Analytics({ isAdmin }) { const store = window.MSStore.useStore(); const t = window.MSStore.useT(); const [lang] = window.MSStore.useLang(); const [range, setRange] = useASt("week"); useAEf(() => { if (!store.analytics && !(store.loading && store.loading.analytics)) { store.loadAnalytics(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const a = store.analytics || null; const isLoading = store.loading && store.loading.analytics; const error = store.errors && store.errors.analytics; const locale = lang === "es" ? "es-ES" : "en-US"; const bars = useAMemo(() => buildBars(range, (a && a.sessions_per_day) || [], locale, t), [range, a, locale, t]); const totalForRange = bars.reduce((s, b) => s + b.count, 0); const diagnoses = (a && a.top_diagnoses) || []; const donutItems = diagnoses.slice(0, 6).map((d, i) => ({ label: d.name, count: d.count, color: PALETTE[i % PALETTE.length] })); const profile = store.profile; const account = store.activeAccount; const subtitle = isAdmin ? (account && account.name) ? t("an.clinicOverview", { clinic: account.name }) : t("an.clinicOverviewBlank") : t("an.yourPractice", { name: (profile && profile.name) ? profile.name.replace(/^Dr\.?\s+/i, "") : "—" }); return (
{/* Header */}

{t("an.title")}

{subtitle}
{[["week", t("common.week")], ["month", t("common.month")], ["quarter", t("common.quarter")]].map(([v, l]) => ( ))}
{error && (
{error}
)} {/* Stat tiles */}
{[ { label: t("an.totalVisits"), value: totalForRange, sub: t("an.totalVisitsSub", { n: bars.length }), icon: }, { label: t("an.avgDuration"), value: a ? `${a.avg_duration_minutes || 0} min` : "—", sub: t("an.avgDurationSub"), icon: }, { label: t("an.visitsThisMonth"), value: a ? a.sessions_this_month || 0 : "—", sub: t("an.visitsThisMonthSub"), icon: }, { label: t("an.uniquePatients"), value: a ? a.total_patients || 0 : "—", sub: t("an.uniquePatientsSub"), icon: }, ].map(stat => (
{stat.label} {stat.icon}
{stat.value}
{stat.sub}
))}
{isAdmin && (
{(store.providers || []).map(p => (
{p.name}
))}
)} {/* Main chart + breakdown */}
{t("an.visitVolume")} {range === "week" ? t("an.daily") : range === "month" ? t("an.weekly") : t("an.monthly")}
{isLoading && !a ? (
{t("an.loading")}
) : ( )}
{t("an.topDiagnoses")} {a && {t("an.fromNotes")}}
{isLoading && !a ? (
{t("an.loading")}
) : donutItems.length === 0 ? (
{t("an.noDiagnoses")}
) : ( )}
{/* Bottom: top medications + transcription accuracy */}
{t("an.topMeds")} {a && {t("an.mostPrescribed")}}
{a && a.top_medications && a.top_medications.length > 0 ? (
{a.top_medications.slice(0, 8).map((m, i) => { const pct = a.top_medications[0] ? Math.round(m.count / a.top_medications[0].count * 100) : 0; return (
{m.name} {m.count}
); })}
) : (
{t("an.noMeds")}
)}
); } window.Analytics = Analytics;