Network Operations Console — Design Spec
Date: 2026-06-12 Status: Design (Phase 0 + Phase 1) Instance: DentoCorrect (isolated) first; pattern applies to any future enterprise org instance.Problem
The Network Hub (/network) today is a vanity rollup — today’s appointments, revenue, no-show rate, staff count. A multi-branch Head of Operations cannot run a network on that. They need to see, across all branches at once: where the money is (billed vs collected vs outstanding), where it’s leaking (no-shows, overdue AR, low collection), and which branch/doctor is underperforming — with drill-down to the why, and targets they’re measured against. The bar is “better than the generic CRM they’re using,” and the CRM knows nothing about a chair, a no-show, or a dentist’s daily production.
Core insight (why this is mostly reuse)
The per-clinic analytics already compute the hard parts — they’re just filtered to a singleclinicId:
routes/analytics.ts → /financial-summary: revenue billed / realized / outstanding (AR), paid/unpaid/overdue invoice counts, gross profit + margin + cost, quotation conversion, monthly trend. Filter is literallyclinicId ? eq(invoices.clinicId, clinicId) : undefined.routes/reports.ts → /financial-statement: cash-basis P&L (receipts − direct cost − operating expenses − payroll).routes/expenses.ts,routes/payroll.ts,routes/inventory.ts: all clinic-scoped CRUD + summaries.appointments+doctor_schedules+rooms: no-shows + utilization.
clinicId IN (branchIds) and GROUP BY clinic (or doctor), plus presentation. ~90% reuse. The only genuinely net-new subsystem (doctor commissions) is out of scope for this spec (Phase 3).
Scope
In scope (Phase 0 + Phase 1):- Phase 0 — Foundation: shared metrics engine, scoped roles, isolated-DB schema sync, targets + alerts.
- Phase 1 — Money modules: Financial Control, No-Show Bleed, Accounting/P&L, Command Center.
Roles & access
org_admin→ network scope: all branches in their organization.- NEW
branch_manager→ branch scope: exactly one branch (theiruser_clinic_assignmentsclinic). Same console UI, server-enforced single-branch scope; no cross-branch comparison table. - Branch staff (admin/doctor/receptionist) keep their existing single-clinic dashboard — unchanged.
clinicId-from-JWT-only rule in financial-summary).
Architecture
1. Shared metrics engine (server/src/lib/metrics/)
Pure functions reading from getAnalyticsDb(), each taking a common options object:
{ totals, groups }):
getFinancialMetrics(scope)— billed, collected, outstanding(AR), AR aging buckets (0–30/31–60/61–90/90+ frominvoices.balance+invoiceDate), gross profit, margin, cost, payment-method mix (payments.paymentMethod), invoice-status counts, daily/monthly trend.getAppointmentMetrics(scope)— booked/completed/cancelled/no_show/missed counts + rates; no-show PKR bleed = no_show count × avg completed-ticket; new vs returning patients; utilization = booked minutes ÷ capacity (doctor_schedules× working days,roomsfor chairs).getAccountingPnL(scope)— cash-basis revenue (receipts), direct cost, operating expenses by category (expenses), payroll total, net profit.
analytics.ts / reports.ts are shared code that also ships to the main app; touching them risks destabilizing it (prime directive: never mess with the main app). The engine is NEW code consumed ONLY by the org-insights routes. It replicates the existing per-clinic formulas exactly, and a parity test asserts engine-single-branch == existing-route output. Converging the old routes onto the engine is an explicit, separately-verified later step — not part of this build.
All time filtering uses AT TIME ZONE 'Asia/Karachi' per the established PKT convention.
2. API surface (server/src/routes/org-insights.ts, mounted /api/v1/org/insights)
Middleware requireInsightsScope resolves c.get('insightsScope') = { clinicIds, level }:
- org_admin → all
organization_clinicsfor their org →level:'network'. - branch_manager →
[their assigned clinicId]→level:'branch'.
?from&to, optional ?branch=<clinicId> to focus one branch within scope):
GET /financial→getFinancialMetricstotals + per-branch rows.GET /appointments→getAppointmentMetrics(bleed, funnel, utilization).GET /accounting→getAccountingPnL.GET /command→ KPI rollup + branch leaderboard + alerts (targets evaluated) + live feed.GET /targets,PUT /targets→ per-branch / network targets (org_admin only).
branch= outside the caller’s clinicIds returns 403. Branch managers cannot widen scope by any parameter.
3. Targets + alerts
New tableapp.ops_targets:
metric ∈ revenue_monthly | noshow_rate_max | collection_rate_min | utilization_min. Created via idempotent CREATE TABLE IF NOT EXISTS ensure-step (matches the sidebar-order precedent), plus a drizzle schema file for type-safety.
Alerts evaluator compares current-period actuals (from the engine) to targets → { metric, clinicId, actual, target, severity }[], surfaced in Command Center and as per-branch badges.
4. Schema sync (prerequisite — gated)
The isolated DentoCorrect DB drifts behind code (missing columns/enum values — confirmed during branch seeding). Before Phase 1 queries rely onreceipts, quotations, expenses, payroll, inventory, doctor_schedules, introspect + sync that DB to current (drizzle-kit push). This touches a live tenant DB → requires explicit per-action confirmation at execution time (no-live-tenant-execution rule). Treated as Phase 0 step 1.
UI (Network Hub tabs)
Premium, drill-down — not plain shadcn wraps (KPI cards with sparklines + vs-target deltas, branch leaderboard, charts). Currency icon =Banknote (never DollarSign). No raw clinic IDs surfaced.
- Command Center (redesign of Overview): network KPIs with actual-vs-target deltas, alert strip, branch leaderboard (rank by revenue / flag worst no-show & AR), network live feed.
- Financial: revenue billed/collected/outstanding, AR aging, profit & margin, payment mix; per-branch table + trend; click a row → that branch’s clinic dashboard (
?clinicId=). - No-Shows: PKR bleed headline, booked→completed funnel, no-show-rate trend, chair/doctor utilization, cancellation reasons.
- Accounting: network P&L (revenue → direct cost → gross → expenses/payroll → net), expense breakdown by category, per-branch comparison.
- Settings → Targets: per-branch + network target editor (org_admin only).
Data flow
Error handling
- Empty branch set → zeroed totals + empty table (never throw), mirrors current
/org/overview. - Out-of-scope
branch=→ 403. - Missing reused table/column (pre-sync) → endpoint returns a typed
degraded: trueflag + which module is unavailable, so the UI shows “needs schema sync” instead of a 500. - All queries wrapped in
handleErrorper existing route convention.
Testing
- Unit: metrics engine functions with a fixture of 2 clinics × known invoices/appointments → assert totals + per-branch split + AR aging boundaries + no-show bleed math.
- Scope: branch_manager cannot read another branch (
branch=403; absent param → only own clinic). - Parity: per-clinic dashboard numbers == network console single-branch numbers (proves the shared engine).
- The seeded DentoCorrect demo data (60 patients / 450 appts / 198 payments) is the live smoke target.
Phasing recap
- Phase 0 — engine + scoped roles + schema sync + targets/alerts.
- Phase 1 — Financial + No-Shows + Accounting + Command Center.
- Phase 2 — Inventory + Payroll + Productivity (reuse).
- Phase 3 — Doctor Commissions (net-new) + automation.

