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
| Hook | Purpose |
|---|---|
useEnrollmentMetrics | Enrollment counts and screen failures |
useEnrollmentTrend | Time-series enrollment data |
useSafetyMetrics | Adverse event counts |
useVisitCompliance | Visit completion rates |
useSitePerformance | Site-level metrics |
useStudyOverview | Study counts by phase/status |
useSubjectVitals | Patient vital signs |
useSubjectVisits | Patient 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
- 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>
);
}
- Add routing in
clinical-dashboard.tsx:
const isNewRole = normalizedRole === "new_role";
if (isNewRole) return <NewRoleDashboard />;
- 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/:
| Component | Usage |
|---|---|
KPICard | Single metric display with trend |
WidgetContainer | Wrapper with title and loading state |
LoadingState | Skeleton loader |
ErrorState | Error display with retry |
GaugeChart | Circular 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"]}}'