// 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 (
);
}
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 => (
{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 (
);
})}
) : (
{t("an.noMeds")}
)}
);
}
window.Analytics = Analytics;