Frontend - UI Components
React component tree, AG Grid integration, tab system, service layer, and event type mapping for the Historical Summary feature
Frontend - UI Components
This page covers the React component tree, AG Grid configuration, the tab system, the service layer, and how the frontend maps user interactions to backend event types.
Component Tree
Entry Point: NewDrillDownView
Source: NewDrillDownView/index.tsx
This component manages the top-level "Summary View / Detailed View / Historical View" tab switching:
- Renders the "Historical View" tab only if
sessionState.enable_operation_summaryis truthy - Manages shared state: date range, granularity, selected centres (multi-centre labs)
- Computes auto-granularity via
getGranularity(), which selects daily/weekly/monthly based on date range length:if (dateDiff <= 14) return "Daily"; else if (dateDiff <= 62) return "Weekly"; else return "Monthly";
HistoricalSummary
Source: HistoricalSummary.tsx
This component owns the tab navigation and delegates data display to HistoricalDetails:
State it manages:
activeEntityTab- Which entity type is selected (Organization/Referral/Branch/Department/Test)activeCountTab- Which count is selected (Test Count / Sample Count / Bill Count)selectedBreakdown- Optional drill-down selection (Test Wise / Sample Wise)accessFilter- Optional access filter (Accessed / Not Accessed)
Tab System:
There are two levels of tabs:
| Level | Config Source | Options |
|---|---|---|
| Entity Type Tabs | entityTypeTabs in constants.ts | Organization Wise, Referral Wise, Department Wise, Test Wise, Branch Wise |
| Count Summary Tabs | countSummaryTabs in constants.ts | Test Count, Sample Count, Bill Count |
Some tabs are hidden based on the selected entity type:
entityTypeTabs = [
{ label: "Organization Wise", hideTabs: [] },
{ label: "Referral Wise", hideTabs: [] },
{ label: "Department Wise", hideTabs: ["Sample Count", "Bill Count"] },
{ label: "Test Wise", hideTabs: ["Sample Count", "Bill Count"] },
{ label: "Branch Wise", hideTabs: [] },
];Department Wise and Test Wise only support Test Count. The backend has no department_sample, department_bill, etc. event types.
Breakdown options (per count type):
historicalBreakdownOptions = {
"Test Count": [{ label: "Test Wise" }],
"Sample Count": [{ label: "Sample Wise" }],
};Bill Count has no breakdown options.
HistoricalDetails
Source: HistoricalDetails.tsx
This component handles:
- Data fetching - calls
getHistoricalSummaryData()whenever props change - Data transformation - pivots flat API response into AG Grid row format
- Synthetic rows - calculates group-level totals and averages
- Pinned rows - grand total and grand average pinned to the bottom
- AG Grid rendering - configures columns, comparators, and cell renderers
AG Grid Configuration
The grid is configured as a pivot table where:
- First column is the entity name (org name, referral name, etc.)
- Dynamic columns are one column per date bucket (e.g., "2026-03-01", "2026-03-08")
- Pinned bottom rows show Grand Total and Grand Average
Column Generation
Columns are generated dynamically from the API response's date buckets:
// Pseudocode for column generation
const dateColumns = uniqueBuckets.map(bucket => ({
field: bucket,
headerName: formatDate(bucket),
valueGetter: createLeafMetricValueGetter(bucket, summaryTypes),
cellRenderer: createBucketOrTotalCellRenderer(bucket, normalRowMode, summaryTypes),
comparator: createSummaryMetricComparator(summaryTypes),
}));Synthetic Row Types
The grid inserts computed rows after each group:
| Row Type | summaryType value | Calculation |
|---|---|---|
| Group Total | "groupTotal" | Sum of all values in the group for each bucket |
| Group Average | "groupAverage" | Average of all values in the group for each bucket |
| Pinned Grand Total | "pinnedGrandTotal" | Sum across all groups (pinned to bottom) |
| Pinned Grand Average | "pinnedGrandAverage" | Average across all groups (pinned to bottom) |
Row Sorting
Summary rows are always sorted to the bottom of their group using createSummaryMetricComparator():
const createSummaryMetricComparator = (types) => (valueA, valueB, nodeA, nodeB, isDescending) => {
const rankA = getSummaryRank(nodeA?.data, types); // 0 = data, 1 = total, 2 = average
const rankB = getSummaryRank(nodeB?.data, types);
if (rankA !== rankB) {
return isDescending ? rankB - rankA : rankA - rankB;
}
return (Number(valueA) || 0) - (Number(valueB) || 0);
};Service Layer
Source: services.ts
getHistoricalSummaryData()
This function builds the API request from the current UI state:
const getHistoricalSummaryData = async (params) => {
const {
eventType, // e.g., "organization_test"
startDate, // "2026-03-01"
endDate, // "2026-03-14"
granularity, // "daily" | "weekly" | "monthly"
breakdown, // optional: "true" for detailed view
secondaryGrouping, // optional: "true" for test-wise
accessFilter, // optional: "accessed" | "not_accessed"
} = params;
const response = await GET(
`/api-v3/operation/operation-dashboard/historical-summary/${eventType}`,
{ start_date, end_date, summary_granularity, ... }
);
return groupByDimension(response.data.summary, granularity);
};groupByDimension()
Transforms the flat API response into the row-based structure AG Grid expects:
Input (from API):
[
{ "dimension_a_name": "City Hospital", "ordered_bucket": "2026-03-01", "total_value": 45 },
{ "dimension_a_name": "City Hospital", "ordered_bucket": "2026-03-08", "total_value": 52 },
{ "dimension_a_name": "Metro Clinic", "ordered_bucket": "2026-03-01", "total_value": 28 }
]Output (for AG Grid):
[
{ "key": "City Hospital", "dimension_a_id": 101, "2026-03-01": 45, "2026-03-08": 52 },
{ "key": "Metro Clinic", "dimension_a_id": 102, "2026-03-01": 28, "2026-03-08": 0 }
]Each ordered_bucket value becomes a column field in the row object.
Event Type Mapping: getEventKey()
Source: helpers.ts
This function translates the UI's tab selections into the backend's event_type string:
// Input: UI tab labels
getEventKey("Test Count", "Organization Wise") // → "organization_test"
getEventKey("Sample Count", "Referral Wise") // → "referral_sample"
getEventKey("Bill Count", "Branch Wise") // → "branch_bill"The mapping uses two lookup tables:
| UI Service Label | Maps to |
|---|---|
| Test Count | "test" |
| Sample Count | "sample" |
| Bill Count | "bill" |
| UI Group Label | Maps to |
|---|---|
| Organization Wise | "organization" |
| Referral Wise | "referral" |
| Department Wise | "department" |
| Test Wise | "organization" ⚠️ |
| Branch Wise | "branch" |
Important quirk: "Test Wise" maps to
"organization"(not"test"), because the event type format is{entity}_{metric}. If "Test Wise" mapped to"test", combining with "Test Count" would produce"test_test", which isn't a valid event type. The "Test Wise" tab actually means "group by test entity" which is achieved viasecondaryGrouping=trueon the API. The event key is used just for the primary grouping.
The generated event type is validated against the VALID_EVENT_TYPES array. If it's not valid, the function returns an empty string and the API call is skipped.
Granularity Auto-Selection
Source: getGranularity() in helpers.ts
export const getGranularity = (startDate, endDate) => {
const dateDiff = endDate.diff(startDate, "days") + 1;
if (dateDiff <= 14) return { label: "Daily", value: 1 };
else if (dateDiff <= 62) return { label: "Weekly", value: 2 };
else return { label: "Monthly", value: 3 };
};This matches the backend's validation limits exactly: daily allows max 14 days, weekly allows max 62 days. The user can also manually select a different granularity via the dropdown (GRANULARITY_OPTIONS).
Notification Banner
export const getHistoricalViewNotificationText = (labTimeZone) => {
return i18n.t(
"All activities related to orders & samples performed today will appear " +
"in the Historical View on the next day. All data is calculated based on " +
"the {{timezone}} timezone.",
{ timezone: labTimeZone }
);
};This notification is always displayed at the top of the Historical View to set user expectations about data latency.
Export Configuration
The grid supports Excel export with a custom processCellCallback:
totalBilledHistoryDetailsExportConfig = {
fileName: `Historical Details-Export ${moment().format("YYYY-MM-DD")}`,
processCellCallback: (params) => {
if (params?.column?.colId === "key") {
return dateFormatChange(params?.value);
}
return params.value;
},
};The key column holds the entity name (organization, referral, etc.). The dateFormatChange() call is a defensive pass-through — it only reformats values that parse as valid dates. Since entity names are never valid date strings, the callback returns them unchanged. This is likely a leftover from an earlier iteration and functions as a no-op in practice.
Source File Index
| File | Purpose |
|---|---|
NewDrillDownView/index.tsx | Parent container: date range, view switching, feature flag check |
HistoricalSummary.tsx | Tab navigation, state management, data fetching orchestration |
HistoricalDetails.tsx | AG Grid rendering, data transformation, synthetic rows |
services.ts | API service layer: request building and response transformation |
constants.ts | Tab definitions, granularity options, breakdown options, export configs |
helpers.ts | getEventKey(), getGranularity(), summary row utilities, access filter logic |