// Screen: Reports — five fully-rendered, interactive reports + grayed builder.
// Charts lazy-mount: only the active report (and active sub-view) is rendered,
// so nothing is drawn into a hidden container.
const REPORT_TYPES = [
{ id: 'prescriber', n: '01', title: 'Prescriber Reports', desc: 'Accounts, trends & actions' },
{ id: 'revenue', n: '02', title: 'Revenue & Growth', desc: 'MTD/QTD/YTD · YoY · mix' },
{ id: 'patients', n: '03', title: 'Patient Analytics', desc: 'Aggregate · PHI-safe' },
{ id: 'volume', n: '04', title: 'Volume & Workflow', desc: 'Fills · turnaround · staff' },
{ id: 'controlled', n: '05', title: 'Controlled Substances', desc: 'C-III–C-V · PDMP' },
];
const fmtCount = n => Math.round(n).toLocaleString('en-US');
const fmtM = n => '$' + (n / 1e6).toFixed(2) + 'M';
// ---- Small local viz primitives ----------------------------------------
function Donut({ data, size = 168, thickness = 26, title, sub }) {
const total = data.reduce((a, d) => a + d.val, 0);
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
let off = 0;
return (
);
}
function DonutBlock({ data, title, sub, fmt }) {
const total = data.reduce((a, d) => a + d.val, 0);
const f = fmt || fmtCount;
return (
{data.map((d, i) => (
{d.name}
{f(d.val)}
{(d.val / total * 100).toFixed(1)}%
))}
);
}
function HBars({ data, fmt }) {
const max = Math.max(...data.map(d => d.val));
const f = fmt || fmtCount;
return (
{data.map((d, i) => (
{d.name}
{d.sub ? {d.sub} : null}
{f(d.val)}
))}
);
}
function Kpis({ items }) {
return (
{items.map((it, i) => (
{it.k}
{it.v}
{it.sub ?
{it.sub}
: null}
))}
);
}
function PeriodSel({ value, onChange, opts }) {
return (
{opts.map(o => (
))}
);
}
function ReportHead({ title, sub, period, children }) {
return (
{title}
{sub ?
{sub}
: null}
{children}
);
}
function ChangeTag({ pct }) {
const cls = pct > 1.5 ? 'up' : pct < -1.5 ? 'down' : 'flat';
const arrow = cls === 'up' ? '▲' : cls === 'down' ? '▼' : '—';
return {arrow} {Math.abs(pct).toFixed(1)}%;
}
// Least-squares fitted line for a dashed trend overlay.
function linfit(data) {
const n = data.length;
let sx = 0, sy = 0, sxx = 0, sxy = 0;
for (let i = 0; i < n; i++) { sx += i; sy += data[i]; sxx += i * i; sxy += i * data[i]; }
const b = (n * sxy - sx * sy) / (n * sxx - sx * sx);
const a = (sy - b * sx) / n;
return data.map((_, i) => +(a + b * i).toFixed(1));
}
// ---- Report 1: Prescriber Reports --------------------------------------
function PrescriberReport({ onPdf }) {
const { useState } = React;
const [period, setPeriod] = useState('90d');
const [selId, setSelId] = useState(null);
const [sortKey, setSortKey] = useState('rev');
const months = PRESC_PERIODS.find(p => p.key === period).months;
const rows = PRESCRIBERS.map(c => {
const rev = prescRev(c, months);
const chg = prescChange(c, months);
return { c, rev, chg };
});
const sorted = [...rows].sort((a, b) => sortKey === 'rev' ? b.rev - a.rev : b.chg - a.chg);
const growing = rows.filter(r => r.chg > 1.5).length;
const declining = rows.filter(r => r.chg < -1.5).length;
const totalRev = rows.reduce((a, r) => a + r.rev, 0);
if (selId) return ;
const totalMonths = MONTHS_12.map((_, i) => +(PRESCRIBERS.reduce((a, c) => a + c.months[i], 0) / 1000).toFixed(1));
const series = [
{ name: 'Total', data: totalMonths, color: 'var(--moss)', bold: true },
{ name: '', data: linfit(totalMonths), color: '#8e9385', dash: true },
];
return (
| Prescribing clinic |
Top category |
setSortKey('rev')}>Revenue ▾ |
setSortKey('chg')}>Recent vs prior ▾ |
{sorted.map(({ c, rev, chg }) => (
setSelId(c.id)}>
|
{c.clinic}
{c.prescriber} · {c.city}
|
{c.topCat} |
{fmtK(rev)} |
|
))}
);
}
function genWeekly(clinic, period) {
const months = PRESC_PERIODS.find(p => p.key === period).months;
const monthlyRev = clinic.months[clinic.months.length - 1];
const sign = clinic.trend === 'up' ? 1 : clinic.trend === 'down' ? -1 : 0;
const labels = ['May 11', 'May 18', 'May 25', 'Jun 1', 'Jun 8'];
const baseScripts = clinic.scripts30 / 4.345;
const baseRev = monthlyRev / 4.345;
return labels.map((lb, i) => {
const g = 1 + sign * (i - 2) * 0.03;
return {
week: lb,
scripts: Math.round(baseScripts * g),
rev: Math.round(baseRev * g / 10) * 10,
newPts: Math.max(0, clinic.newPerWeek + ((i % 2 === 0) ? 0 : (sign >= 0 ? 1 : -1))),
};
});
}
function PrescriberSingle({ id, setId, period, setPeriod, onPdf }) {
const idx = PRESCRIBERS.findIndex(c => c.id === id);
const c = PRESCRIBERS[idx];
const months = PRESC_PERIODS.find(p => p.key === period).months;
const rev = prescRev(c, months);
const scripts = Math.round(c.scripts30 * rev / c.months[c.months.length - 1]);
const avg = rev / scripts;
const series = [
{ name: c.prescriber.split(' ').slice(-1)[0], data: c.months.map(v => +(v / 1000).toFixed(1)), color: 'var(--moss)', bold: true },
{ name: '', data: linfit(c.months.map(v => +(v / 1000).toFixed(1))), color: '#8e9385', dash: true },
];
const weeks = genWeekly(c, period);
const prev = () => setId(PRESCRIBERS[(idx - 1 + PRESCRIBERS.length) % PRESCRIBERS.length].id);
const next = () => setId(PRESCRIBERS[(idx + 1) % PRESCRIBERS.length].id);
return (
{PRESCRIBERS.map(p => (
))}
Account
{c.clinic}
{c.prescriber} · {c.city}
Revenue · {period}
{fmtK(rev)}
Scripts
{fmtCount(scripts)}
Avg / script
{fmtMoney(avg)}
Trend analysis
{c.analysis}
Recommended action
{c.rec}
| Week | Scripts | Revenue | New patients |
{weeks.map((w, i) => (
| {w.week} |
{w.scripts} |
{fmtMoney(w.rev)} |
{w.newPts} |
))}
);
}
// ---- Report 2: Revenue & Growth ----------------------------------------
function RevenueReport({ onPdf }) {
const series = [
{ name: 'This yr', data: REV_THIS_YEAR, color: 'var(--moss)', bold: true },
{ name: 'Last yr', data: REV_LAST_YEAR, color: '#8e9385' },
];
const movers = [...BIGGEST_MOVERS]
.map(m => ({ ...m, pct: (m.recent - m.prior) / m.prior * 100 }))
.sort((a, b) => Math.abs(b.recent - b.prior) - Math.abs(a.recent - a.prior));
return (
'$' + v.toFixed(1) + 'k'}>
| Item | 90d revenue | Change |
{movers.map((m, i) => (
|
{m.name}
{m.cat}
|
{fmtK(m.recent)} |
|
))}
);
}
// ---- Report 3: Patient Analytics ---------------------------------------
function PatientReport({ onPdf }) {
const a = PATIENT_ANALYTICS;
const series = [{ name: 'New', data: a.newPerMonth, color: 'var(--moss)', bold: true }];
return (
i % 2 === 0 ? m : '')} fmt={fmtCount}>
| Gender | Patients | Share |
{a.gender.map((g, i) => (
| {g[0]} |
{fmtCount(g[1])} |
{g[2]} |
))}
);
}
// ---- Report 4: Volume & Workflow ---------------------------------------
function VolumeReport({ onPdf }) {
const { useState } = React;
const [period, setPeriod] = useState('30d');
const v = VOLUME;
const fs = fillsSeries(period);
const series = [{ name: 'Fills', data: fs.data, color: 'var(--moss)', bold: true }];
const bars = v.pharmacists.map(p => ({ name: p.name, sub: p.store, val: p.fills }));
return (
p.key === period).label + ' · stores closed weekends'}>
);
}
// ---- Report 5: Controlled Substance Compliance -------------------------
function ControlledReport({ onPdf }) {
const k = CONTROLLED;
const series = [{ name: 'Controlled', data: k.perMonth, color: 'var(--moss)', bold: true }];
const schedTone = { 'C-III': 'warn', 'C-IV': 'neutral', 'C-V': 'ok' };
return (
i % 2 === 0 ? m : '')} fmt={fmtCount}>
| Item | Schedule | Fills · YTD | Last dispensed |
{k.items.map((it, i) => (
| {it.name} |
{it.sched} |
{fmtCount(it.fills)} |
{it.lastStore} |
))}
{k.pdmp}
);
}
// ---- Shell -------------------------------------------------------------
function Reports() {
const { useState } = React;
const [active, setActive] = useState('prescriber');
const [toast, showToast] = useToast();
const onPdf = () => showToast('Available in full build.');
const pane = {
prescriber: ,
revenue: ,
patients: ,
volume: ,
controlled: ,
}[active];
return (
{REPORT_TYPES.map(r => (
))}
06
Custom builder Phase 2
Drag-and-drop report designer
{pane}
Aggregate data only · PHI-safe
);
}
Object.assign(window, { Reports });