Skip to main content

Dashboard Development Guide

This guide covers the technical implementation of role-based dashboards in the CTMS web application.


Architecture Overview

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ Cube.js API │────▶│ React Hooks │────▶│ Dashboard │
│ (REST) │ │ (use-dashboard)│ │ Components │
│ localhost:4001 │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘

All dashboards fetch data from Cube.js semantic layer via custom React hooks.


File Structure

src/
├── components/dashboard/
│ ├── clinical-dashboard.tsx # Main router component
│ ├── admin-dashboard.tsx # Platform Administrator
│ ├── designer-dashboard.tsx # Study Designer
│ ├── coordinator-dashboard.tsx # Study Coordinator
│ ├── subject-dashboard.tsx # Patient/Subject
│ ├── date-range-picker.tsx # Shared date picker
│ └── widgets/ # Reusable chart components
├── hooks/
│ └── use-dashboard.ts # Cube.js query hooks
└── lib/
└── clients/
└── cube-client.ts # Cube.js API client

Role-Based Routing

The clinical-dashboard.tsx component routes users to their role-specific dashboard:

// Determine dashboard type based on normalized role
const isPatient = normalizedRole === "patient";
const isDesigner = normalizedRole === "study_designer";
const isCoordinator = normalizedRole.includes("coordinator");

// Render appropriate dashboard
if (isPatient) return <SubjectDashboard />;
if (isDesigner) return <DesignerDashboard />;
if (isCoordinator) return <CoordinatorDashboard />;
return <AdminDashboard />;

Creating Custom Hooks

Dashboard hooks are defined in src/hooks/use-dashboard.ts.

Basic Hook Pattern

export function useEnrollmentMetrics(
dateRange: [string, string] | undefined,
options?: UseCubeQueryOptions
) {
const query = useMemo(
() => ClinicalQueries.enrollmentMetrics(dateRange),
[dateRange]
);

return useCubeQuery<Record<string, unknown>>(query, options);
}

Key Hooks Available

HookPurpose
useEnrollmentMetricsEnrollment counts and screen failures
useEnrollmentTrendTime-series enrollment data
useSafetyMetricsAdverse event counts
useVisitComplianceVisit completion rates
useSitePerformanceSite-level metrics
useStudyOverviewStudy counts by phase/status
useSubjectVitalsPatient vital signs
useSubjectVisitsPatient appointments

Cube.js Query Client

The cube-client.ts provides pre-built queries:

export const ClinicalQueries = {
enrollmentMetrics: (dateRange?: [string, string]): CubeQuery => ({
measures: [
"Enrollments.subjectCount",
"Enrollments.screenFailureCount",
],
timeDimensions: dateRange ? [{
dimension: "Enrollments.enrollmentDate",
dateRange,
}] : undefined,
}),
// ... more queries
};

Environment Configuration

NEXT_PUBLIC_CUBE_API_URL=http://localhost:4001

Adding a New Dashboard

  1. Create the component in src/components/dashboard/:
export function NewRoleDashboard() {
const metrics = useEnrollmentMetrics(dateRange);

return (
<div className="space-y-6 p-6">
<KPICard title="Total" value={metrics.data?.total} />
</div>
);
}
  1. Add routing in clinical-dashboard.tsx:
const isNewRole = normalizedRole === "new_role";
if (isNewRole) return <NewRoleDashboard />;
  1. Update role mapping in src/app/home/page.tsx:
if (userRoles.includes(ROLES.NEW_ROLE)) {
return "new_role";
}

Reusable Widget Components

Located in src/components/dashboard/widgets/:

ComponentUsage
KPICardSingle metric display with trend
WidgetContainerWrapper with title and loading state
LoadingStateSkeleton loader
ErrorStateError display with retry
GaugeChartCircular progress indicator

KPICard Example

<KPICard
title="Total Enrolled"
value={formatCompactNumber(enrolled)}
subtitle="Subjects in studies"
icon={Users}
isLoading={isLoading}
/>

Data Constraints

Patient Email Resolution

// Use EMAIL as unique key (not name) since names can be duplicate
// Flow: Email → Patients.email → patientId → Enrollments.usubjid
const { usubjid } = useSubjectIdFromEmail(profile?.email);

Multiple Study Enrollments

// Patient can participate in >1 study (active or historical)
// Return array of enrollments for multi-study patients

Most Recent Vitals

// Vitals are per encounter - pick most recent reading per test type
// Uses VitalSigns cube ordered by measurementDate DESC

Testing

Run the development server with Cube.js:

# Terminal 1: Cube.js
cd ctms-semantic-cube && npm run dev

# Terminal 2: Web app
cd hb-life-science-web && bun run dev

Verify Cube.js connectivity:

curl -s "http://localhost:4001/cubejs-api/v1/load" \
-H "Content-Type: application/json" \
-d '{"query": {"measures": ["Enrollments.subjectCount"]}}'