// Dashboard page — Bloomberg-dense KPI overview for Raudhah clinic group // Data source: window.RAUDHAH_DATA (server-rendered from FastAPI) const D = window.RAUDHAH_DATA; const URLS = window.RAUDHAH_URLS || { dashboard: "/", entry: "/entry", targets: "/targets", branch: "/branch/" }; // ---- Summary KPIs ------------------------------------------------------- function KpiSummary({ mode, branches, windowSize }) { const last = D.weeks.length - 1; const start = Math.max(0, last - windowSize + 1); const sumAt = (obj, wk) => branches.reduce((a, b) => a + ((obj[b.id] && obj[b.id][wk]) || 0), 0); const fbSeries = D.weeks.map((_, i) => sumAt(D.fb, i)); const waSeries = D.weeks.map((_, i) => sumAt(D.wa, i)); const ttSeries = D.weeks.map((_, i) => sumAt(D.tt, i)); const igSeries = D.ig.total || []; function windowBaseline(series) { for (let i = start; i <= last; i++) { if (series[i] != null) return { value: series[i], idx: i }; } return { value: null, idx: null }; } const cards = [ { key: "fb", label: "FB FOLLOWERS", swatch: "var(--raudhah-pink)", series: fbSeries, color: "var(--raudhah-pink)" }, { key: "wa", label: "WA MEMBERS", swatch: "var(--teal)", series: waSeries, color: "var(--teal)" }, { key: "tt", label: "TIKTOK FOLLOWERS", swatch: "var(--b-bs)", series: ttSeries, color: "var(--b-bs)" }, { key: "ig", label: "IG FOLLOWERS", swatch: "var(--b-mn)", series: igSeries, color: "var(--b-mn)" }, ]; // Branch view: hide IG (not branch-scoped) const visibleCards = mode === "branch" ? cards.filter(c => c.key !== "ig") : cards; const grVals = Object.values(D.gr).filter(g => g); const grTotal = grVals.reduce((a, b) => a + (b.total || 0), 0); const grRating = grTotal ? (grVals.reduce((a, b) => a + (b.rating || 0) * (b.total || 0), 0) / grTotal).toFixed(2) : "—"; const pendingEntries = (D.missing || []).length; const newBranchCount = D.BRANCHES.filter(b => b.isNew).length; return (
{visibleCards.map(c => { const value = c.series[last] || 0; const baseline = windowBaseline(c.series); const d = baseline.value != null ? value - baseline.value : null; const pct = (baseline.value != null && baseline.value !== 0) ? +((d / baseline.value) * 100).toFixed(2) : null; const since = baseline.idx != null && baseline.idx > start ? D.weeks[baseline.idx] : null; const sparkSlice = c.series.slice(start); return (
{c.label}
{value.toLocaleString()}
{d == null ? : } {since && since {since.slice(5)}}
); })}
GOOGLE REVIEWS · TOTAL
{grTotal.toLocaleString()}
AVG RATING
{grRating}
ACTIVE BRANCHES
{D.BRANCHES.length}{newBranchCount ? · {newBranchCount} new : null}
WEEKLY ENTRIES DUE
{pendingEntries}pending
); } // ---- Multi-branch line chart (ApexCharts) ------------------------------- function LineChart({ id, title, eyebrow, sourceKey, branches, windowSize = 12, height = 240, foot }) { const ref = useRef(null); const chartRef = useRef(null); const [theme, setTheme] = useState(document.documentElement.getAttribute("data-theme") || "light"); useEffect(() => { const obs = new MutationObserver(() => setTheme(document.documentElement.getAttribute("data-theme") || "light")); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); return () => obs.disconnect(); }, []); useEffect(() => { if (!ref.current || !window.ApexCharts) return; const last = D.weeks.length - 1; const start = Math.max(0, last - windowSize + 1); const slicedWeeks = D.weeks.slice(start); const series = (sourceKey === "ig") ? [{ name: "IG", data: (D.ig.total || []).slice(start).map((v, i) => ({ x: slicedWeeks[i], y: v })), color: "var(--b-mn)" }] : branches.map(b => ({ name: b.code, data: ((D[sourceKey] && D[sourceKey][b.id]) || []).slice(start).map((v, i) => ({ x: slicedWeeks[i], y: v })), color: b.hex, })); const opts = { chart: { type: "line", height, toolbar: { show: false }, animations: { enabled: true, speed: 500 }, fontFamily: "Poppins", background: "transparent", parentHeightOffset: 0, events: { markerClick: (_, __, cfg) => { const br = branches[cfg.seriesIndex]; if (br && br.slug) location.href = URLS.branch + br.slug; }, }, }, theme: { mode: theme }, series, stroke: { curve: "smooth", width: 2 }, colors: sourceKey === "ig" ? ["var(--b-mn)"] : branches.map(b => b.hex), markers: { size: 0, hover: { size: 5 } }, grid: { borderColor: "var(--grid-line-strong)", strokeDashArray: 3, padding: { left: 10, right: 10, top: 0, bottom: 0 } }, xaxis: { type: "datetime", labels: { style: { colors: "var(--fg3)", fontSize: "10px", fontFamily: "JetBrains Mono" }, datetimeFormatter: { month: "MMM dd" } }, axisBorder: { show: false }, axisTicks: { show: false }, }, yaxis: { labels: { style: { colors: "var(--fg3)", fontSize: "10px", fontFamily: "JetBrains Mono" }, formatter: v => v == null ? "" : Math.round(v).toLocaleString() }, }, legend: { show: false }, tooltip: { theme, x: { format: "dd MMM yyyy" }, y: { formatter: v => v == null ? "—" : v.toLocaleString() } }, dataLabels: { enabled: false }, }; chartRef.current = new ApexCharts(ref.current, opts); chartRef.current.render(); return () => chartRef.current && chartRef.current.destroy(); }, [sourceKey, branches.map(b=>b.id).join(","), theme, windowSize]); return (
{eyebrow}

{title}

{(sourceKey === "ig" ? [{ id: "ig", code: "IG", hex: "#6A8E3E" }] : branches).map(b => ( {b.code} ))}
{foot &&
{foot}
}
); } // ---- Target attainment panel ------------------------------------------- function TargetAttainment({ branches }) { const rows = branches.map(b => { const tgt = (D.targets || []).filter(t => t.kpi === "fb_followers" && t.branch === b.id); tgt.sort((a, c) => a.value - c.value); const primary = tgt[0]; const stretch = tgt[1]; const value = (D.fb[b.id] || []).slice(-1)[0] || 0; return { b, value, target: primary ? primary.value : null, target2: stretch ? stretch.value : null }; }).filter(r => r.target != null); if (!rows.length) { return (
Target attainment

Facebook follower targets

No FB targets set yet.
Set targets →
); } return (
Target attainment

Facebook follower targets

Primary + stretch · progress to due date
Manage →
{rows.map((r, i) => ( {r.b.name}} value={r.value} target={r.target} target2={r.target2} color={r.b.hex} onClick={() => r.b.slug && (location.href = URLS.branch + r.b.slug)} /> ))}
); } // ---- Ranking bars ------------------------------------------------------- function Ranking({ title, eyebrow, sourceKey, format = v => v.toLocaleString(), branches }) { const data = branches.map(b => ({ b, value: sourceKey === "gr" ? (D.gr[b.id] ? D.gr[b.id].total : 0) : ((D[sourceKey] && D[sourceKey][b.id]) ? D[sourceKey][b.id].slice(-1)[0] || 0 : 0), })).sort((a, b) => b.value - a.value); const max = Math.max(1, ...data.map(d => d.value)); return (
{eyebrow}

{title}

{data.map(d => (
d.b.slug && (location.href = URLS.branch + d.b.slug)}>
0.35 ? "white" : "var(--fg1)", textShadow: d.value / max > 0.35 ? "0 1px 1px rgba(0,0,0,0.2)" : "none" }}> {d.b.name}
{format(d.value)}
))}
); } // ---- Google reviews stacked --------------------------------------------- function ReviewBreakdown({ branches }) { const entries = branches.map(b => ({ b, g: D.gr[b.id] })).filter(x => x.g); const maxTotal = Math.max(1, ...entries.map(x => x.g.total)); return (
Google Reviews

Star distribution by branch

Stacked 5★ → 1★ · width proportional to review count
5★ 4★ 3★ 2★ 1★
{entries.map(({ b, g }) => { const width = (g.total / maxTotal * 100); return (
b.slug && (location.href = URLS.branch + b.slug)}>
{[5,4,3,2,1].map(s => { const n = g.stars ? (g.stars[s] || 0) : 0; const pct = g.total ? (n / g.total) * 100 : 0; return ; })}
{g.rating != null ? g.rating.toFixed(1) : "—"}★ {(g.total || 0).toLocaleString()} reviews
); })}
); } // ---- Posting heatmap ---------------------------------------------------- function PostingHeatmap({ branches, mode }) { const labs14 = D.postingWeeks || []; const [tab, setTab] = useState("fb"); const channelRows = (D.posting && D.posting[tab]) || []; // Branch filter: when in single-branch mode, keep only rows whose label prefix matches the branch code let rows = channelRows; if (mode === "branch" && branches.length === 1) { const code = branches[0].code; rows = channelRows.filter(r => r.label.split("·")[0] === code); } else if (mode === "hq") { // HQ: show DR·TT row in TT tab, HQ·TH row in TH tab; FB tab effectively empty if (tab === "fb") rows = []; else if (tab === "tt") rows = channelRows.filter(r => r.label.startsWith("DR")); else if (tab === "th") rows = channelRows; // single HQ·TH row } return (
Posting consistency

Weekly posts · 14-week heatmap

0 posts → 5+ posts per week · gaps flagged in alerts
{["fb", "tt", "th"].map(t => ( ))}
Less {[0,1,2,3,4,5].map(v => ( ))} More
{rows.length === 0 ? (
No data for this filter
) : ( <>
{labs14.map(l => {l})} Σ
{rows.map(row => (
{row.label}
{row.values.map((v, i) => (
))}
{row.values.reduce((a,b)=>a+b,0)}
))}
)}
); } // ---- Alerts side-rail --------------------------------------------------- function AlertsRail() { const [dismissed, setDismissed] = useState(() => new Set()); const allAlerts = D.alerts || []; const visibleAlerts = allAlerts.filter(a => !dismissed.has(`${a.group}::${a.text}::${a.sub}`)); const groups = {}; visibleAlerts.forEach(a => { (groups[a.group] = groups[a.group] || []).push(a); }); const tickChar = { bad: "!", warn: "▲", info: "i" }; const onClickAlert = (a) => { if (a.branch) { location.href = URLS.branch + a.branch; } else { location.href = URLS.entry; } }; const dismiss = (a, e) => { e.stopPropagation(); const key = `${a.group}::${a.text}::${a.sub}`; setDismissed(prev => { const next = new Set(prev); next.add(key); return next; }); }; return ( ); } // ---- Snapshot strip (compact ticker) ----------------------------------- function SnapshotStrip({ branches, windowSize }) { const last = D.weeks.length - 1; const start = Math.max(0, last - windowSize + 1); const baselineAt = (series) => { for (let i = start; i <= last; i++) { if (series[i] != null) return series[i]; } return null; }; const cells = branches.map(b => { const fbSeries = D.fb[b.id] || []; const waSeries = D.wa[b.id] || []; const ttSeries = D.tt[b.id] || []; const fb = fbSeries[last] || 0; const wa = waSeries[last] || 0; const tt = ttSeries[last] || 0; const gr = (D.gr[b.id] && D.gr[b.id].total) || 0; const rating = (D.gr[b.id] && D.gr[b.id].rating) || null; const fbBase = baselineAt(fbSeries); const waBase = baselineAt(waSeries); const ttBase = baselineAt(ttSeries); return { b, fb, wa, tt, gr, rating, fbDelta: fbBase != null ? fb - fbBase : 0, waDelta: waBase != null ? wa - waBase : 0, ttDelta: ttBase != null ? tt - ttBase : 0, }; }); return (
Snapshot
Week ending
{D.weekLabel}
{cells.map((c, i) => (
c.b.slug && (location.href = URLS.branch + c.b.slug)}>
{c.b.name} {c.b.isNew && NEW}
{[ { k: "FB", v: c.fb, d: c.fbDelta }, { k: "WA", v: c.wa, d: c.waDelta }, { k: "TT", v: c.tt, d: c.ttDelta }, ].map(row => (
{row.k} {row.v.toLocaleString()}
))}
GR {c.gr.toLocaleString()} {c.rating != null && ( {c.rating.toFixed(1)}★ )}
))}
); } // ---- HQ panels ---------------------------------------------------------- function HqKpiSummary({ windowSize }) { const last = D.weeks.length - 1; const start = Math.max(0, last - windowSize + 1); const igSeries = D.ig.total || []; const drSeries = (D.tt && D.tt.doctor) || []; const thFollowersSeries = D.thFollowers || []; const thViewsSeries = D.thViews || []; function windowBaseline(series) { for (let i = start; i <= last; i++) if (series[i] != null) return { value: series[i], idx: i }; return { value: null, idx: null }; } const cards = [ { key: "ig", label: "IG FOLLOWERS", swatch: "var(--b-mn)", series: igSeries, color: "var(--b-mn)" }, { key: "dr", label: "DOCTOR TIKTOK", swatch: "var(--b-bs)", series: drSeries, color: "var(--b-bs)" }, { key: "thf", label: "THREADS · FOLLOWERS", swatch: "#1f2933", series: thFollowersSeries, color: "#1f2933" }, { key: "thv", label: "THREADS · VIEWS", swatch: "#1f2933", series: thViewsSeries, color: "#1f2933", isSum: true }, ]; return (
{cards.map(c => { const value = c.isSum ? c.series.slice(start).reduce((a, b) => a + (b || 0), 0) : (c.series[last] || 0); const baseline = windowBaseline(c.series); let d = null, pct = null; if (c.isSum) { const prevWindow = c.series.slice(Math.max(0, start - windowSize), start).reduce((a, b) => a + (b || 0), 0); d = value - prevWindow; pct = prevWindow ? +((d / prevWindow) * 100).toFixed(2) : null; } else if (baseline.value != null) { d = value - baseline.value; pct = baseline.value ? +((d / baseline.value) * 100).toFixed(2) : null; } return (
{c.label}
{value.toLocaleString()}
{d == null ? : }
); })}
); } function HqTargets() { const hqTargets = (D.targets || []).filter(t => (t.kpi === "tiktok" && t.account === "doctor") || t.kpi === "ig" ); if (hqTargets.length === 0) { return (
HQ targets

IG · Doctor TT

No HQ targets set yet.
Set targets →
); } return (
HQ targets

IG · Doctor TT

Active HQ targets
Manage →
{hqTargets.map((t, i) => { let value = 0; let label = ""; if (t.kpi === "tiktok") { value = ((D.tt && D.tt.doctor) || []).slice(-1)[0] || 0; label = "Doctor TikTok followers"; } else if (t.kpi === "ig") { value = (D.ig.total || []).slice(-1)[0] || 0; label = "Instagram followers"; } return ( ); })}
); } function HqRecentPosts() { const items = []; (D.th || []).forEach(p => items.push({ type: "TH", date: p.date, label: p.topic, extra: p.views != null ? `${p.views} views` : "" })); (D.doctorRecent || []).forEach(p => items.push({ type: "TT", date: p.date, label: p.title, extra: "" })); items.sort((a, b) => (a.date < b.date ? 1 : -1)); const top = items.slice(0, 10); return (
HQ posts

Recent activity

Last 10 posts across Threads + Doctor TikTok
{top.length === 0 ? (
No recent posts logged.
) : (
{top.map((p, i) => (
{p.type}
{p.label}
{p.date}{p.extra ? " · " + p.extra : ""}
))}
)}
); } function HqGrowthRanking({ windowSize }) { const last = D.weeks.length - 1; const start = Math.max(0, last - windowSize + 1); function windowBaseline(series) { for (let i = start; i <= last; i++) if (series[i] != null) return series[i]; return null; } const ig = D.ig.total || []; const dr = (D.tt && D.tt.doctor) || []; const th = D.thViews || []; const igCur = ig[last] || 0, igBase = windowBaseline(ig); const drCur = dr[last] || 0, drBase = windowBaseline(dr); const thWindow = th.slice(start).reduce((a, b) => a + (b || 0), 0); const thPrior = th.slice(Math.max(0, start - windowSize), start).reduce((a, b) => a + (b || 0), 0); const rows = [ { label: "Instagram", delta: igBase != null ? igCur - igBase : 0, hex: "var(--b-mn)" }, { label: "Doctor TT", delta: drBase != null ? drCur - drBase : 0, hex: "var(--b-bs)" }, { label: "Threads views", delta: thWindow - thPrior, hex: "#1f2933" }, ].sort((a, b) => b.delta - a.delta); const max = Math.max(1, ...rows.map(r => Math.abs(r.delta))); return (
HQ growth

By channel · this window

{rows.map(r => (
{r.label}
{r.delta >= 0 ? "+" : ""}{r.delta.toLocaleString()}
))}
); } // ---- Dashboard assembly ------------------------------------------------- function Dashboard() { const [branchFilter, setBranchFilter] = useState("all"); const [timeframe, setTimeframe] = useState("12w"); const HQ_CHIP = { id: "hq", code: "HQ", name: "Headquarters", hex: "#1f2933", isHq: true }; const FILTER_CHIPS = [ { id: "all", code: "ALL", hex: "var(--fg2)" }, HQ_CHIP, ...D.BRANCHES, ]; const mode = branchFilter === "all" ? "all" : (branchFilter === "hq" ? "hq" : "branch"); const activeBranch = mode === "branch" ? D.BRANCHES.find(b => b.id === branchFilter) : null; const N = { "4w": 4, "12w": 12, "YTD": 52 }[timeframe] || 12; const visible = mode === "all" ? D.BRANCHES : mode === "hq" ? (D.HQ_ACCOUNTS || []) : D.BRANCHES.filter(b => b.id === branchFilter); const branchScope = mode === "branch" ? [activeBranch] : D.BRANCHES; const totals = { fb: D.BRANCHES.reduce((a, b) => a + ((D.fb[b.id] || []).slice(-1)[0] || 0), 0), wa: D.BRANCHES.reduce((a, b) => a + ((D.wa[b.id] || []).slice(-1)[0] || 0), 0), tt: Object.values(D.tt).reduce((a, arr) => a + ((arr || []).slice(-1)[0] || 0), 0), gr: Object.values(D.gr).reduce((a, g) => a + (g.total || 0), 0), }; return (

Group Dashboard

{mode === "all" && <>{D.BRANCHES.length} branches · } {mode === "branch" && <>{activeBranch.name} · } {mode === "hq" && <>IG · Doctor TT · Threads · } {N === 52 ? "52-week" : timeframe} view ending {D.weekLabel} · {totals.fb.toLocaleString()} FB · {totals.wa.toLocaleString()} WA · {totals.tt.toLocaleString()} TikTok · {totals.gr.toLocaleString()} Google Reviews
{FILTER_CHIPS.map(c => ( ))}
{["4w", "12w", "YTD"].map(t => ( ))}
{mode === "hq" ? ( <>
) : ( <>
)}
); } Object.assign(window, { Dashboard });