Overview

The Issue Insights Dashboard is a comprehensive analytics module within the Nimbly audit admin system that provides real-time visibility into operational issues across multiple sites. It offers both basic and advanced dashboard views with sophisticated filtering capabilities, data visualization, and detailed issue tracking.

Key Features

  • Real-time issue tracking and monitoring
  • Multi-dimensional filtering system
  • Interactive data visualizations
  • Responsive design for mobile and desktop
  • Saved filter preferences
  • Export capabilities
  • Performance-optimized data loading

Architecture

The Issue Insights Dashboard follows a modular architecture pattern with clear separation of concerns:

File Structure

src/
├── pages/dashboardRevamp/
│   ├── issueInsightsPage/
│   │   ├── IssueInsightsPage.tsx      # Main page component
│   │   ├── IssueInsightSummary.tsx    # Summary metrics component
│   │   ├── IssueInsightTable.tsx      # Table view component
│   │   ├── useIssueInsightsData.ts    # Data fetching hook
│   │   ├── IssueStatus.tsx            # Status distribution chart
│   │   ├── IssuePriority.tsx          # Priority distribution chart
│   │   └── IssueDetails.tsx           # Detailed analytics component
│   ├── DashboardFilters.tsx           # Shared filter component
│   ├── OperationalPageHeader.tsx      # Common header component
│   ├── dashboardUtils.ts              # Shared utilities
│   └── utils/
│       ├── useDashboardDataOptions.ts # Filter options hook
│       └── isEmptyValue.ts           # Validation utilities
├── reducers/dashboardRevamp/
│   ├── issue-insights/                # Redux state management
│   │   ├── issueInsights.action.ts
│   │   ├── issueInsights.actionTypes.ts
│   │   ├── issueInsights.reducer.ts
│   │   └── issueInsightsStore.ts
│   └── operational/                   # Parent dashboard state
│       └── operationalStore.ts
├── services/dashboardRevamp/
│   └── issueInsights/                 # API service layer
│       └── issueInsights.service.ts
└── components/
    ├── dashboardRevamp/
    │   ├── ViewDetailsModal/          # Detailed view modal
    │   ├── DrillModal/                # Drill-down modals
    │   └── ChartInfo.tsx              # Chart tooltips
    └── global/
        ├── FilterBar/                 # Filter UI components
        └── LoadingDots/               # Loading indicators

Technology Stack

  • Frontend Framework: React 17.x with TypeScript
  • State Management: Redux with Redux-Saga for side effects
  • Styling: Styled-components + Tailwind CSS
  • Data Visualization: Recharts library
  • API Communication: Fetch API with custom authentication
  • Routing: React Router v5
  • Internationalization: i18next
  • Form Handling: React Hook Form
  • Date Handling: Moment.js

Routes and Navigation

Primary Routes

Route PathComponentAccess ControlDescription
/analytics/operational/issue-insightsOperationalIssueInsightsDashboardPageRoleResources.DASHBOARD_OVERVIEW_ALLMain issue insights dashboard view
/analytics/operationalOperationalDashboardPageRoleResources.DASHBOARD_OVERVIEW_ALLParent operational dashboard
/analytics/executiveExecutiveDashboardPageRoleResources.DASHBOARD_OVERVIEW_ALLExecutive level dashboard
/issuesIssuesPageFeature: ISSUE_TRACKERIssue tracker listing page
/issues/:issueIdIssuesDetailsPageFeature: ISSUE_TRACKERIndividual issue details
graph TD
    A[Executive Dashboard] --> B[Operational Dashboard]
    B --> C[Issue Insights Dashboard]
    C --> D[Issue Details Modal]
    C --> E[Issue Tracker Page]
    C --> F[Download Reports]
    D --> G[Drill-down Views]
    G --> H[Site Level]
    G --> I[Department Level]
    G --> J[User Level]

Desktop View:

  1. Dashboard → Operational KPI Dashboard → Issue Insights Dashboard

Mobile View:

  1. Operational KPI Dashboard → Issue Insights Dashboard

Deep Linking Support

The dashboard supports deep linking through query parameters for:

Components

Main Page Component

IssueInsightsPage

Location: src/pages/dashboardRevamp/issueInsightsPage/IssueInsightsPage.tsx

The main container component that orchestrates the entire Issue Insights Dashboard.

Key Responsibilities:

  • Filter management and persistence
  • Data fetching coordination
  • Layout management (responsive design)
  • Child component composition

State Management:

  • Local state for temporary filter values
  • Redux integration for global filters
  • Filter saving/loading from backend

Core Components

1. IssueStatus Component

Location: src/pages/dashboardRevamp/issueInsightsPage/IssueStatus.tsx

Visualizes issue distribution by status using an interactive doughnut chart.

Features:

  • Color-coded status segments
  • Click interactions for filtering
  • Responsive design variations
  • Loading state handling

Data Flow:

graph LR
    A[Redux State] --> B[IssueStatus]
    B --> C[DoughnutPieChart]
    C --> D[Click Event]
    D --> E[Dispatch Action]
    E --> A

2. IssueDetails Component

Location: src/pages/dashboardRevamp/issueInsightsPage/IssueDetails.tsx

Provides detailed issue analytics with multi-dimensional drill-down capabilities.

Tab Views:

  • Severity
  • Priority
  • Flag
  • Secondary Status
  • Approval Status

Drill-down Hierarchy:

  1. Level 1: Overview by selected dimension
  2. Level 2: Breakdown by site/department
  3. Level 3: Further breakdown by sub-categories

Key Features:

  • Breadcrumb navigation
  • Radio button drill-by selection
  • Download functionality (PDF, CSV, XLSX)
  • View details modal integration

3. IssueInsightTable Component

Location: src/pages/dashboardRevamp/issueInsightsPage/IssueInsightTable.tsx

Tabular representation of issue data with advanced filtering and export capabilities.

Tab Options:

  • Issue Count: Displays issue counts grouped by selected dimension
  • Issue Resolution Time: Shows average resolution times

Table Features:

  • Dynamic column configuration based on groupBy selection
  • Pagination with customizable page sizes (10, 25, 50, 100)
  • Column selector for custom views
  • Export functionality

Column Mappings:

Group ByAvailable Columns
SiteSite Name, Issue Count, Resolution Time
DepartmentDepartment Name, Issue Count, Resolution Time
UserUser Name, Email, Issue Count, Resolution Time
CategoryCategory Name, Issue Count, Resolution Time
QuestionnaireQuestionnaire Name, Issue Count, Resolution Time

4. IssueInsightSummary Component

Location: src/pages/dashboardRevamp/issueInsightsPage/IssueInsightSummary.tsx

High-level metrics dashboard with drill-down capabilities.

Metrics Displayed:

  • Issue Opened: Total new issues in period
  • Issue Closed: Total resolved issues
  • Issue Reoccurred: Reopened issues count
  • Average IRT: Mean resolution time
  • Min/Max IRT: Resolution time range
  • Department with Highest IRT: Slowest department

Interactive Elements:

  • Click metrics to open drill-down modals
  • Trend indicators (up/down arrows)
  • Period-over-period comparisons

Supporting Components

DashboardFilters

Location: src/components/dashboardRevamp/DashboardFilters.tsx

Reusable filter component providing:

  • Date range selection
  • Multi-select dropdowns
  • Filter persistence
  • Reset functionality

ViewDetailsModal

Location: src/components/dashboardRevamp/ViewDetailsModal/

Modal component for detailed data viewing with:

  • Configurable columns
  • Search functionality
  • Export options
  • Pagination

IssueReportDrillModal

Location: src/components/dashboardRevamp/DrillModal/IssueReportDrillModal.tsx

Specialized modal for drilling into specific metrics:

  • Opened issues detail
  • Closed issues detail
  • Reoccurred issues detail

API Integration

API Endpoints

EndpointMethodPurposeParameters
/statistics/dashboard/issue/detailPOSTFetch issue insights summaryDashboard filters
/statistics/dashboard/issuePOSTFetch issue list/chart datafilters, metric, viewBy, groupBy, limit, page
/statistics/dashboard/issue/download/viewPOSTPreview download datagroupBy, fieldName[], filters
/statistics/dashboard/issue/downloadPOSTDownload file (CSV/Excel)groupBy, fieldName[], fileType, filters
/issues/issues/bulkUpdatePUTBulk update issuesissueIDs[], data, triggerNotification
/issues/issueMessages/bulkCreatePOSTBulk create issue messagesissueIDs[], data, triggerNotification
/issues/issue-tracker-filtersGET/POST/PUT/DELETEManage custom filtersfilterId (for update/delete)

API Service Architecture

graph TD
    A[IssueInsightsPage] --> B[Redux Actions]
    B --> C[Redux Saga]
    C --> D[API Service Layer]
    D --> E[Backend API]
    E --> F[Database]
    
    D --> G[fetchIssueIDList]
    D --> H[fetchSavedFilters]
    D --> I[fetchSaveFilters]
    
    C --> J[issueInsights.actionSaga.ts]
    J --> K[API Calls with Authorization]

Request/Response Flow

1. Fetching Issue Details

// Request
POST /statistics/dashboard/issue/detail
{
  sites: ["site1", "site2"],
  departments: ["dept1"],
  startDate: "2024-01-01",
  endDate: "2024-01-31",
  questionnaires: ["q1", "q2"]
}
 
// Response
{
  issueOpened: 150,
  issueClosed: 120,
  issueReoccured: 30,
  avgIRT: 24.5,
  minIRT: 2,
  maxIRT: 72,
  deptWithHighestIRT: "Operations"
}

2. Fetching Chart Data

// Request
POST /statistics/dashboard/issue
{
  metric: "issuePieChart",
  viewBy: "severity",
  groupBy: "site",
  drillBy: ["site1"],
  status: "all",
  sortBy: "percentage"
}
 
// Response
[
  {
    name: "High",
    value: 45,
    percentage: 30
  },
  {
    name: "Medium",
    value: 60,
    percentage: 40
  }
]

Authentication

All API calls include:

  • Authorization: Bearer token from Firebase Auth
  • Content-Type: application/json
  • Organization context from user profile

Error Handling

  • Network errors trigger retry with exponential backoff
  • Auth errors redirect to login
  • API errors display toast notifications
  • Loading states prevent duplicate requests

State Management

Redux Store Structure

The Issue Insights Dashboard uses Redux for centralized state management with the following structure:

interface IssueInsightsState {
  filters: IssueInsightsFilters;
  issueInsightsData: IssueInsightsData | null;
  issueCountList: any[];
  issueChartList: any[];
  issueStatusData: any[];
  issueDetailsChartFilters: {
    viewBy: string;
    groupBy: string;
    drillBy: string[];
    status: string;
    metric: string;
  };
  isLoading: boolean;
  isIssueChartLoading: boolean;
  isIssueCountListLoading: boolean;
  page: number;
  limit: number;
  groupBy: string;
}

Action Types

enum IssueInsightsActionTypes {
  FETCH_ISSUE_INSIGHT_DETAIL = '@dashboardRevamp/issueInsights/FETCH_ISSUE_INSIGHT_DETAIL',
  FETCH_ISSUE_INSIGHT_LIST = '@dashboardRevamp/issueInsights/FETCH_ISSUE_INSIGHT_LIST',
  FETCH_ISSUE_CHART = '@dashboardRevamp/issueInsights/FETCH_ISSUE_CHART',
  FETCH_ISSUE_TABLE_DATA = '@dashboardRevamp/issueInsights/FETCH_ISSUE_TABLE_DATA',
  SET_FILTERS = '@dashboardRevamp/issueInsights/SET_FILTERS',
  CLEAR_FILTERS = '@dashboardRevamp/issueInsights/CLEAR_FILTERS',
  SET_PAGE = '@dashboardRevamp/issueInsights/SET_PAGE',
  SET_ISSUE_STATUS = '@dashboardRevamp/issueInsights/SET_ISSUE_STATUS',
}

Data Flow Diagram

stateDiagram-v2
    [*] --> ComponentMount
    ComponentMount --> LoadSavedFilters
    LoadSavedFilters --> FetchData
    
    FetchData --> LoadingState
    LoadingState --> DataReceived
    DataReceived --> DisplayData
    
    DisplayData --> UserInteraction
    UserInteraction --> UpdateFilters
    UpdateFilters --> FetchData
    
    UserInteraction --> SaveFilters
    SaveFilters --> PersistToBackend
    
    UserInteraction --> ResetFilters
    ResetFilters --> LoadDefaultFilters
    LoadDefaultFilters --> FetchData

Redux-Saga Effects

The module uses Redux-Saga for handling side effects:

  1. fetchIssueInsightDetailSaga: Fetches summary metrics
  2. fetchIssueInsightListSaga: Fetches list data for tables
  3. fetchIssueChartSaga: Fetches chart visualization data
  4. watchIssueInsights: Root saga watching all actions

State Update Patterns

// Filter Update Flow
1. User selects filter → Component state update
2. Apply button → Dispatch Redux actions
3. Saga intercepts → API call
4. Response → Update Redux state
5. Components re-render with new data

Filters and Data Flow

Filter Architecture

Filter Types

1. Global Operational Filters

  • Date Range (startDate, endDate)
  • Site Selection (siteIDs)
  • Department Selection (departmentIDs)
  • User Selection (userIDs)
  • Questionnaire Selection (questionnaireIDs)
  • Categories
  • Roles
  • Region Selection (siteRegionIDs)

2. Issue-Specific Filters

  • Primary Status (OPEN, RESOLVED, CANCELED)
  • Secondary Status (PENDING, OVERDUE, ON_TIME, BEHIND_TIME)
  • Approval Status
  • Priority (HIGH, MEDIUM, LOW)
  • Issue ID (specific issue selection)
  • Issue Source (REPORT, AUDIT, INSPECTION)

Filter Dependencies

graph TD
    A[Primary Status] --> B[Secondary Status Options]
    B --> C{Status = OPEN?}
    C -->|Yes| D[PENDING, OVERDUE]
    C -->|No| E{Status = RESOLVED?}
    E -->|Yes| F[ON_TIME, BEHIND_TIME]
    E -->|No| G[All Options]

Filter Persistence

Save Filter Flow:

  1. User configures filters
  2. Clicks “Save Filters” button
  3. System calls fetchSaveFilters API
  4. Filters stored in backend with user context
  5. Success toast notification

Load Filter Flow:

  1. Page loads
  2. System calls fetchSavedFilters
  3. If saved filters exist:
    • Apply to Redux state
    • Show notification modal
    • Update UI components
  4. If no saved filters:
    • Use default 7-day range
    • All other filters empty

Filter Application Logic

// Filter Application Process
const handleFilterOnApply = (filters: Partial<OperationalFilters>) => {
  const additionalFilters = {
    ...filters,
    primaryStatus: tempStatus,
    secondaryStatus: tempSecondaryStatus,
    approvalStatus: tempApprovalStatus,
    priority: tempPriority,
    issueIDs: tempIssueIDs,
    issueSource: tempIssueSource,
  };
  
  // Update both filter stores
  dispatch(setFilters(additionalFilters));
  dispatch(setIssueFilters(additionalFilters));
  
  // Refresh all dashboard data
  fetchData();
  setLastUpdated(moment());
};

Filter Reset Logic

When resetting filters:

  1. Fetch parent dashboard (KPI) filters
  2. Clear all issue-specific filters
  3. Remove saved filter preferences
  4. Reset pagination to page 1
  5. Refresh data with defaults

Filter UI Components

FilterField Component Features:

  • Multi-select capability
  • Search within options
  • Select/Deselect all
  • Placeholder management
  • Disabled state handling

Filter Grouping:

  • Common filters in DashboardFilters wrapper
  • Issue-specific filters as child components
  • Responsive layout for mobile/desktop

Dependencies

Core Dependencies

{
  "react": "^17.0.2",
  "react-dom": "^17.0.2",
  "redux": "^4.1.2",
  "react-redux": "^7.2.6",
  "redux-saga": "^1.2.1",
  "react-router-dom": "^5.3.0",
  "typescript": "^4.5.4"
}

UI and Styling

{
  "styled-components": "^5.3.3",
  "tailwindcss": "^3.0.23",
  "@nimbly-technologies/audit-component": "latest",
  "react-icons": "^4.3.1",
  "classnames": "^2.3.1"
}

Data Visualization

{
  "recharts": "^2.1.9",
  "@nimbly-technologies/nimbly-common": "latest"
}

Utilities

{
  "moment": "^2.29.1",
  "moment-timezone": "^0.5.34",
  "query-string": "^7.1.0",
  "lodash": "^4.17.21",
  "i18next": "^21.6.11",
  "react-i18next": "^11.15.3"
}

Advanced Features

1. Drill-Down Navigation

The dashboard supports multi-level drill-down navigation:

// Drill hierarchy example
Site Level → Department Level → User Level → Individual Issues
 
// Implementation
const handleDrillDown = (item: NodeData) => {
  const currentLevel = drillByFields.length;
  const nextDrillBy = getNextDrillByField(currentLevel);
  
  if (nextDrillBy) {
    const newDrillByFields = [...drillByFields, item._id];
    loadDrillIntoData({
      groupBy: nextDrillBy,
      drillByFields: newDrillByFields
    });
  }
};

2. Dynamic Chart Configuration

Charts adapt based on data and user preferences:

interface ChartConfig {
  type: 'doughnut' | 'bar' | 'line';
  colors: string[];
  dataKey: string;
  nameKey: string;
  showLegend: boolean;
  showTooltip: boolean;
}
 
// Dynamic color mapping
const COLOR_MAP = {
  HIGH: '#FF5D6E',
  MEDIUM: '#F6BB42',
  LOW: '#54CF68',
  OPEN: '#FF8A96',
  RESOLVED: '#55DA6A',
  CANCELED: '#A0A4A8'
};

3. Export Functionality

Multiple export formats with customizable columns:

// Export configuration
const exportConfig = {
  formats: ['PDF', 'CSV', 'XLSX'],
  defaultColumns: ['site', 'issueCount', 'avgResolutionTime'],
  customColumns: true,
  batchSize: 1000,
  asyncExport: true
};
 
// Export implementation
const handleExport = async (format: ExportFormat) => {
  const data = await fetchExportData({
    filters: currentFilters,
    columns: selectedColumns,
    format
  });
  
  downloadFile(data, `issues_${moment().format('YYYY-MM-DD')}.${format}`);
};

4. Real-time Updates

Dashboard supports real-time updates via WebSocket connection:

const useRealTimeUpdates = () => {
  const [socket, setSocket] = useState(null);
  
  useEffect(() => {
    const ws = new WebSocket(WS_URL);
    
    ws.on('issueUpdate', (data) => {
      dispatch(updateIssueData(data));
    });
    
    return () => ws.close();
  }, []);
};

5. Custom Filter Sets

Users can save and manage custom filter configurations:

interface CustomFilterSet {
  id: string;
  name: string;
  filters: IssueInsightsFilters;
  isDefault: boolean;
  createdAt: string;
  updatedAt: string;
}
 
// Custom filter management
const filterSetManager = {
  save: (name: string, filters: IssueInsightsFilters) => {},
  load: (id: string) => {},
  delete: (id: string) => {},
  setDefault: (id: string) => {}
};

Performance Optimization

1. Data Pagination

All data lists implement pagination to reduce load:

// Pagination configuration
const PAGINATION_CONFIG = {
  defaultPageSize: 10,
  pageSizeOptions: [10, 25, 50, 100],
  maxPageSize: 100,
  prefetchNext: true
};

2. Memoization

Component memoization prevents unnecessary re-renders:

// Memoized selectors
const selectFilteredIssues = createSelector(
  [selectIssues, selectFilters],
  (issues, filters) => filterIssues(issues, filters)
);
 
// Memoized components
const MemoizedIssueChart = React.memo(IssueChart, (prev, next) => {
  return prev.data === next.data && prev.filters === next.filters;
});

3. Lazy Loading

Components load on-demand:

const IssueDetailsModal = lazy(() => 
  import('./components/IssueDetailsModal')
);
 
const IssueExportModal = lazy(() => 
  import('./components/IssueExportModal')
);

4. Debounced API Calls

Filter changes are debounced to reduce API calls:

const debouncedFetchData = useMemo(
  () => debounce(fetchData, 500),
  [fetchData]
);
 
useEffect(() => {
  debouncedFetchData(filters);
}, [filters]);

5. Virtual Scrolling

Large lists use virtual scrolling:

// Virtual list implementation
<VirtualList
  height={600}
  itemCount={issues.length}
  itemSize={50}
  renderItem={({ index, style }) => (
    <IssueRow style={style} issue={issues[index]} />
  )}
/>

Color Scheme and Styling

Theme Configuration

const dashboardTheme = {
  colors: {
    primary: '#574FCF',      // Purple
    success: '#54CF68',      // Green
    warning: '#F6BB42',      // Yellow
    danger: '#ED5565',       // Red
    neutral: {
      100: '#FFFFFF',
      200: '#FAFAFA',
      300: '#EFEEED',
      400: '#EDEDED',
      500: '#E2E2E2',
      600: '#C4C4C4',
      700: '#A0A4A8',
      800: '#535353',
      900: '#25282B'
    }
  },
  breakpoints: {
    mobile: '500px',
    tablet: '768px',
    laptop: '1024px',
    desktop: '1280px'
  },
  spacing: {
    xs: '4px',
    sm: '8px',
    md: '16px',
    lg: '24px',
    xl: '32px'
  }
};

Responsive Design

The dashboard implements a mobile-first responsive design:

// Responsive utilities
const mobileView = window.innerWidth < 500;
const tabletView = window.innerWidth < 768;
const desktopView = window.innerWidth >= 1280;
 
// Conditional rendering
{mobileView ? (
  <MobileLayout>
    <CompactFilters />
    <StackedCharts />
  </MobileLayout>
) : (
  <DesktopLayout>
    <ExpandedFilters />
    <GridCharts />
  </DesktopLayout>
)}

Testing Considerations

Unit Testing

// Component testing example
describe('IssueInsightsPage', () => {
  it('should load saved filters on mount', async () => {
    const { getByText } = render(<IssueInsightsPage />);
    await waitFor(() => {
      expect(fetchSavedFilters).toHaveBeenCalled();
    });
  });
 
  it('should apply filters when Apply button clicked', () => {
    const { getByText } = render(<IssueInsightsPage />);
    fireEvent.click(getByText('Apply'));
    expect(mockDispatch).toHaveBeenCalledWith(setFilters(expect.any(Object)));
  });
});

Integration Testing

// API integration test
describe('Issue Insights API', () => {
  it('should fetch issue data with filters', async () => {
    const filters = { startDate: '2024-01-01', endDate: '2024-01-31' };
    const response = await fetchIssueInsights(filters);
    
    expect(response).toHaveProperty('issueOpened');
    expect(response).toHaveProperty('issueClosed');
    expect(response).toHaveProperty('avgIRT');
  });
});

Security Considerations

Authentication

  • All API calls include Firebase Auth bearer tokens
  • Token refresh handled automatically
  • Role-based access control enforced

Data Validation

  • Input sanitization for filter values
  • XSS prevention in rendered content
  • SQL injection prevention on backend

Permissions

// Permission checks
const canViewDashboard = hasPermission(user, 'DASHBOARD_OVERVIEW_ALL');
const canExportData = hasPermission(user, 'EXPORT_REPORTS');
const canSaveFilters = hasPermission(user, 'SAVE_PREFERENCES');

Accessibility Features

ARIA Support

  • Proper ARIA labels for all interactive elements
  • Screen reader announcements for data updates
  • Keyboard navigation support

Keyboard Shortcuts

ShortcutAction
Ctrl/Cmd + FOpen filters
Ctrl/Cmd + EExport data
Ctrl/Cmd + RRefresh data
EscClose modals
TabNavigate through elements

Focus Management

// Focus trap in modals
const modalRef = useRef(null);
useFocusTrap(modalRef, isModalOpen);
 
// Announce changes to screen readers
const announceUpdate = (message: string) => {
  const announcement = document.createElement('div');
  announcement.setAttribute('role', 'status');
  announcement.setAttribute('aria-live', 'polite');
  announcement.textContent = message;
  document.body.appendChild(announcement);
  setTimeout(() => announcement.remove(), 1000);
};

Monitoring and Analytics

Performance Metrics

// Performance monitoring
const trackDashboardPerformance = () => {
  performance.mark('dashboard-start');
  
  // After data loads
  performance.mark('dashboard-end');
  performance.measure('dashboard-load', 'dashboard-start', 'dashboard-end');
  
  const measure = performance.getEntriesByName('dashboard-load')[0];
  analytics.track('Dashboard Performance', {
    loadTime: measure.duration,
    module: 'issue-insights'
  });
};

User Analytics

  • Filter usage patterns
  • Most viewed metrics
  • Export frequency
  • Drill-down paths

Troubleshooting Guide

Common Issues and Solutions

1. Dashboard Not Loading

Symptoms: Blank screen or loading spinner stuck Solutions:

  • Check network connectivity
  • Verify authentication status
  • Clear browser cache
  • Check console for errors

2. Filters Not Working

Symptoms: Applied filters don’t affect data Solutions:

  • Ensure date range is valid
  • Check filter dependencies
  • Verify API response format
  • Reset filters and try again

3. Export Failing

Symptoms: Export button not working or downloads empty file Solutions:

  • Check selected columns
  • Verify data exists for filters
  • Check browser download settings
  • Try different export format

4. Chart Rendering Issues

Symptoms: Charts appear broken or don’t display Solutions:

  • Update browser to latest version
  • Disable browser extensions
  • Check for JavaScript errors
  • Verify data format from API

Debug Tools

// Enable debug mode
localStorage.setItem('DEBUG_ISSUE_INSIGHTS', 'true');
 
// Debug utilities
window.issueInsightsDebug = {
  getState: () => store.getState().dashboardIssueInsights,
  getFilters: () => store.getState().dashboardIssueInsights.filters,
  forceRefresh: () => dispatch(fetchData()),
  clearCache: () => localStorage.clear()
};

API Error Codes

CodeDescriptionResolution
400Invalid filter parametersCheck filter values
401Authentication failedRe-login required
403Permission deniedCheck user permissions
404Data not foundVerify filter criteria
429Rate limit exceededWait and retry
500Server errorContact support

Glossary

TermDefinition
IRTIssue Resolution Time - Time taken to resolve an issue
Primary StatusMain status of issue (Open, Resolved, Canceled)
Secondary StatusSub-status providing more detail
Drill-downNavigate to more detailed data view
Group ByAggregate data by specified dimension
View ByPrimary dimension for visualization

References

External Documentation

Internal Resources

Version History

VersionDateChanges
1.0.02023-01Initial release
1.1.02023-06Added drill-down navigation
1.2.02023-09Filter persistence feature
1.3.02024-01Performance optimizations
1.4.02024-03Export enhancements

Implementation Details

Saga Architecture

The Issue Insights module uses Redux-Saga for managing side effects with a consistent pattern across all operations:

Base Saga Pattern

function* fetchIssueInsightDetailSaga(
  action: ReturnType<typeof fetchIssueInsightDetailAsync.request>
): Generator {
  try {
    const res: unknown = yield call(fetchIssueInsightDetails);
    yield put(fetchIssueInsightDetailAsync.success(res as IssueInsightsData));
  } catch {
    yield put(
      fetchIssueInsightDetailAsync.failure('[ERROR] Error Fetching issue insights detail')
    );
  }
}

Saga Effects Structure

// Root saga composition
export function* watchIssueInsights() {
  yield takeEvery(
    fetchIssueInsightDetailAsync.request, 
    fetchIssueInsightDetailSaga
  );
  yield takeEvery(
    fetchIssueInsightListAsync.request, 
    fetchIssueInsightListSaga
  );
  yield takeEvery(
    fetchIssueChartAsync.request, 
    fetchIssueChartSaga
  );
  yield takeEvery(
    fetchIssueTableDataAsync.request, 
    fetchIssueTableDataSaga
  );
}

Utility Functions Architecture

Label Mapping System

export const issueStatusLabelMap: { [key: string]: string } = {
  'Open': 'OPEN',
  'open': 'OPEN',
  'Resolved': 'RESOLVED',
  'resolved': 'RESOLVED',
  'Canceled': 'CANCELED',
  'canceled': 'CANCELED',
  'In-review': 'IN_REVIEW',
  'in-review': 'IN_REVIEW',
  'Overdue': 'OVERDUE',
  'overdue': 'OVERDUE',
  'Pending': 'PENDING',
  'pending': 'PENDING',
  'On-time': 'ON_TIME',
  'on-time': 'ON_TIME',
  'Behind-time': 'BEHIND_TIME',
  'behind-time': 'BEHIND_TIME'
};

Column Configuration System

interface ColumnConfig {
  fieldName: string;
  labelToken: string;
  type: 'Number' | 'Percentage' | 'String' | 'Time';
  subColumns?: ColumnConfig[];
  isDefault?: boolean;
}
 
// Example configuration
export const IssueCountColumnConfiguration: ColumnMapping = {
  site: [
    {
      fieldName: 'site.name',
      labelToken: 'label.siteName',
      type: 'String',
      isDefault: true
    },
    {
      fieldName: 'totalReportCount',
      labelToken: 'label.issueCount',
      type: 'Number',
      isDefault: true
    }
  ],
  department: [
    // Similar structure
  ]
};

Service Layer Implementation

Query Building Strategy

const buildQueryParams = (filters: OperationalFilters) => {
  const queryObj: QueryParams = {};
  
  // Date range handling
  if (filters.startDate) queryObj.startDate = filters.startDate;
  if (filters.endDate) queryObj.endDate = filters.endDate;
  
  // Array parameter handling
  if (filters.siteIDs?.length) {
    queryObj['siteIDs[]'] = filters.siteIDs;
  }
  
  // Managed by exclusion
  const { managedByDeptIDs, managedByUserIDs, ...filteredParams } = queryObj;
  
  return filteredParams;
};

Request Configuration

const getRequestOptions = async (data: any, isInternalToken = false) => {
  const headers = new Headers();
  headers.append('Content-Type', 'application/json');
  
  if (isInternalToken) {
    headers.append('Authorization', `Bearer ${internalAPIToken}`);
  } else {
    const user = auth.currentUser;
    const idToken = await user?.getIdToken();
    headers.append('Authorization', `Bearer ${idToken}`);
  }
  
  return {
    method: 'POST',
    headers,
    body: JSON.stringify(data)
  };
};

Data Processing Pipeline

Response Processing

const processIssueData = (response: APIResponse): ProcessedData => {
  // Extract nested data
  const rawData = response?.data?.issueList || [];
  
  // Transform data structure
  return rawData.map(item => ({
    id: item._id,
    name: item.name || 'Unknown',
    count: item.issueCount || 0,
    percentage: calculatePercentage(item.issueCount, totalCount),
    metadata: {
      avgResolutionTime: item.avgIRT || 0,
      minResolutionTime: item.minIRT || 0,
      maxResolutionTime: item.maxIRT || 0
    }
  }));
};

Aggregation Logic

const aggregateIssueMetrics = (issues: Issue[]): AggregatedMetrics => {
  const metrics = issues.reduce((acc, issue) => {
    // Status aggregation
    acc.byStatus[issue.status] = (acc.byStatus[issue.status] || 0) + 1;
    
    // Priority aggregation
    acc.byPriority[issue.priority] = (acc.byPriority[issue.priority] || 0) + 1;
    
    // Time calculations
    if (issue.resolutionTime) {
      acc.totalResolutionTime += issue.resolutionTime;
      acc.resolvedCount++;
    }
    
    return acc;
  }, {
    byStatus: {},
    byPriority: {},
    totalResolutionTime: 0,
    resolvedCount: 0
  });
  
  return {
    ...metrics,
    avgResolutionTime: metrics.totalResolutionTime / metrics.resolvedCount
  };
};

Advanced Filter Implementation

Filter Dependency Management

class FilterDependencyManager {
  private dependencies = new Map<string, string[]>();
  
  constructor() {
    // Define filter dependencies
    this.dependencies.set('primaryStatus', ['secondaryStatus']);
    this.dependencies.set('sites', ['departments', 'users']);
    this.dependencies.set('departments', ['users']);
  }
  
  getDependentFilters(changedFilter: string): string[] {
    return this.dependencies.get(changedFilter) || [];
  }
  
  validateFilterCombination(filters: Filters): ValidationResult {
    // Validate filter combinations
    if (filters.primaryStatus?.includes('OPEN') && 
        filters.secondaryStatus?.includes('ON_TIME')) {
      return {
        valid: false,
        error: 'Invalid status combination'
      };
    }
    return { valid: true };
  }
}

Dynamic Filter Options

const getSecondaryStatusOptions = (
  primaryStatus: string[],
  currentSelection: string[]
): Option[] => {
  const statusMap = {
    OPEN: ['PENDING', 'OVERDUE'],
    RESOLVED: ['ON_TIME', 'BEHIND_TIME'],
    CANCELED: ['CANCELED']
  };
  
  // Get allowed options based on primary status
  const allowedOptions = primaryStatus.flatMap(status => 
    statusMap[status] || []
  );
  
  // Build options with proper state
  return Object.values(secondaryStatusMap).map(option => ({
    ...option,
    isDisabled: !allowedOptions.includes(option.value),
    isSelected: currentSelection.includes(option.value)
  }));
};

Chart Integration Details

Chart Plugin System

// Custom plugin for doughnut chart center text
const doughnutCenterTextPlugin = {
  id: 'doughnutCenterText',
  beforeDraw: (chart: Chart) => {
    const { ctx, width, height } = chart;
    ctx.restore();
    
    const fontSize = (height / 114).toFixed(2);
    ctx.font = `${fontSize}em sans-serif`;
    ctx.textBaseline = 'middle';
    
    const text = chart.config.options.plugins.doughnutCenterText.text;
    const textX = Math.round((width - ctx.measureText(text).width) / 2);
    const textY = height / 2;
    
    ctx.fillText(text, textX, textY);
    ctx.save();
  }
};
 
// Register plugin
Chart.register(doughnutCenterTextPlugin);

Dynamic Chart Configuration

const getChartConfig = (data: ChartData, options: ChartOptions): ChartConfiguration => {
  const colors = data.map(item => COLOR_MAP[item.name] || '#C4C4C4');
  
  return {
    type: 'doughnut',
    data: {
      labels: data.map(d => d.name),
      datasets: [{
        data: data.map(d => d.value),
        backgroundColor: colors,
        borderWidth: 0
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: {
          display: options.showLegend,
          position: 'bottom'
        },
        tooltip: {
          callbacks: {
            label: (context) => {
              const value = context.parsed;
              const percentage = ((value / total) * 100).toFixed(1);
              return `${context.label}: ${value} (${percentage}%)`;
            }
          }
        }
      }
    }
  };
};

Performance Monitoring Implementation

Component Performance Tracking

const useComponentPerformance = (componentName: string) => {
  const [metrics, setMetrics] = useState<PerformanceMetrics>({});
  
  useEffect(() => {
    // Mark component mount
    performance.mark(`${componentName}-mount-start`);
    
    return () => {
      // Mark component unmount
      performance.mark(`${componentName}-mount-end`);
      
      // Measure lifecycle
      performance.measure(
        `${componentName}-lifecycle`,
        `${componentName}-mount-start`,
        `${componentName}-mount-end`
      );
      
      // Get and log metrics
      const measure = performance.getEntriesByName(`${componentName}-lifecycle`)[0];
      console.log(`${componentName} lifecycle:`, measure.duration);
    };
  }, [componentName]);
  
  const trackOperation = (operationName: string, operation: () => void) => {
    const startMark = `${componentName}-${operationName}-start`;
    const endMark = `${componentName}-${operationName}-end`;
    
    performance.mark(startMark);
    operation();
    performance.mark(endMark);
    
    performance.measure(operationName, startMark, endMark);
  };
  
  return { trackOperation, metrics };
};

Error Boundary Implementation

class IssueInsightsErrorBoundary extends React.Component<Props, State> {
  state = {
    hasError: false,
    error: null,
    errorInfo: null
  };
  
  static getDerivedStateFromError(error: Error) {
    return { hasError: true };
  }
  
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log to error reporting service
    console.error('Issue Insights Error:', {
      error: error.toString(),
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString(),
      user: getCurrentUser(),
      filters: this.props.filters
    });
    
    // Send to analytics
    analytics.track('Dashboard Error', {
      module: 'issue-insights',
      error: error.message,
      stack: error.stack
    });
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <ErrorFallback
          onReset={() => this.setState({ hasError: false })}
          error={this.state.error}
        />
      );
    }
    
    return this.props.children;
  }
}

Testing Patterns

Integration Test Example

describe('Issue Insights Integration', () => {
  let store: MockStore;
  
  beforeEach(() => {
    store = mockStore({
      dashboardIssueInsights: {
        filters: DEFAULT_FILTERS,
        issueInsightsData: null,
        isLoading: false
      }
    });
  });
  
  it('should fetch data on filter change', async () => {
    const { getByRole } = render(
      <Provider store={store}>
        <IssueInsightsPage />
      </Provider>
    );
    
    // Open filters
    fireEvent.click(getByRole('button', { name: /filters/i }));
    
    // Change date range
    const startDateInput = getByRole('textbox', { name: /start date/i });
    fireEvent.change(startDateInput, { target: { value: '2024-01-01' } });
    
    // Apply filters
    fireEvent.click(getByRole('button', { name: /apply/i }));
    
    // Verify API calls
    await waitFor(() => {
      const actions = store.getActions();
      expect(actions).toContainEqual(
        expect.objectContaining({
          type: fetchIssueInsightDetailAsync.request.type
        })
      );
    });
  });
});

Component Test Pattern

describe('IssueStatus Component', () => {
  const mockData = [
    { name: 'Open', value: 50, percentage: 50 },
    { name: 'Resolved', value: 30, percentage: 30 },
    { name: 'Canceled', value: 20, percentage: 20 }
  ];
  
  it('should render chart with correct data', () => {
    const { container } = render(
      <IssueStatus data={mockData} isLoading={false} />
    );
    
    // Verify chart canvas exists
    const canvas = container.querySelector('canvas');
    expect(canvas).toBeInTheDocument();
    
    // Verify data labels
    mockData.forEach(item => {
      expect(screen.getByText(item.name)).toBeInTheDocument();
    });
  });
  
  it('should handle click interactions', () => {
    const onStatusClick = jest.fn();
    
    render(
      <IssueStatus 
        data={mockData} 
        isLoading={false}
        onStatusClick={onStatusClick}
      />
    );
    
    // Simulate chart segment click
    const chartElement = screen.getByTestId('issue-status-chart');
    fireEvent.click(chartElement);
    
    expect(onStatusClick).toHaveBeenCalled();
  });
});

Contributing

Development Setup

# Clone repository
git clone https://github.com/Nimbly-Technologies/audit-admin.git
 
# Install dependencies
npm install
 
# Start development server
npm start
 
# Run tests
npm test
 
# Build for production
npm run build

Code Standards

  • Follow TypeScript best practices
  • Write comprehensive tests
  • Document complex logic
  • Use meaningful variable names
  • Keep components focused and small

Pull Request Process

  1. Create feature branch
  2. Implement changes
  3. Add/update tests
  4. Update documentation
  5. Submit PR with description
  6. Address review feedback

Mobile-Specific Implementation

Responsive Component Architecture

Mobile Layout Strategy

const MobileIssueInsightsLayout = () => {
  const isMobile = useMediaQuery('(max-width: 1024px)');
  
  if (!isMobile) return null;
  
  return (
    <div className="aa-flex aa-flex-col aa-gap-2">
      {/* Stacked components for mobile */}
      <IssueInsightSummary />
      <IssueStatus />
      <IssueDetails />
      {/* Fixed scroll buffer */}
      <div className="aa-h-[8vh] aa-block" />
    </div>
  );
};

Touch Interaction Handling

const useTouchInteractions = () => {
  const [touchStart, setTouchStart] = useState(0);
  const [touchEnd, setTouchEnd] = useState(0);
  
  const handleTouchStart = (e: TouchEvent) => {
    setTouchStart(e.targetTouches[0].clientX);
  };
  
  const handleTouchMove = (e: TouchEvent) => {
    setTouchEnd(e.targetTouches[0].clientX);
  };
  
  const handleTouchEnd = () => {
    if (!touchStart || !touchEnd) return;
    
    const distance = touchStart - touchEnd;
    const isLeftSwipe = distance > 50;
    const isRightSwipe = distance < -50;
    
    if (isLeftSwipe) {
      // Navigate to next tab/section
    } else if (isRightSwipe) {
      // Navigate to previous tab/section
    }
  };
  
  return {
    handleTouchStart,
    handleTouchMove,
    handleTouchEnd
  };
};

Internationalization (i18n) Implementation

Translation Structure

// Translation keys structure
const issueInsightsTranslations = {
  en: {
    'label.dashboardRevamp.issueInsights.issueInsightsDashboard': 'Issue Insights Dashboard',
    'label.dashboardRevamp.issueInsights.primaryStatusTooltip': 'Primary status indicates the main state of the issue',
    'label.dashboardRevamp.issueInsights.secondaryStatusTooltip': 'Secondary status provides additional detail about issue state',
    'label.dashboardRevamp.issueInsights.priority': 'Priority',
    'label.dashboardRevamp.issueInsights.issueSource': 'Issue Source',
    'label.dashboardRevamp.issueInsights.childFilterNote': 'Note: This dashboard inherits filters from the parent dashboard',
    // ... more translations
  },
  es: {
    // Spanish translations
  },
  id: {
    // Indonesian translations
  }
};

Dynamic Translation Loading

const useTranslations = () => {
  const { t, i18n } = useTranslation();
  const [isLoading, setIsLoading] = useState(true);
  
  useEffect(() => {
    const loadNamespace = async () => {
      try {
        await i18n.loadNamespaces(['dashboard', 'common']);
        setIsLoading(false);
      } catch (error) {
        console.error('Failed to load translations:', error);
        setIsLoading(false);
      }
    };
    
    loadNamespace();
  }, [i18n]);
  
  return { t, isLoading };
};

Data Caching Strategy

Cache Implementation

class IssueInsightsCache {
  private cache = new Map<string, CacheEntry>();
  private maxAge = 5 * 60 * 1000; // 5 minutes
  
  set(key: string, data: any) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
  }
  
  get(key: string): any | null {
    const entry = this.cache.get(key);
    
    if (!entry) return null;
    
    const age = Date.now() - entry.timestamp;
    if (age > this.maxAge) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.data;
  }
  
  generateKey(filters: Filters): string {
    return JSON.stringify({
      ...filters,
      _type: 'issue-insights'
    });
  }
  
  clear() {
    this.cache.clear();
  }
}
 
// Usage in saga
function* fetchWithCache(filters: Filters) {
  const cacheKey = cache.generateKey(filters);
  const cachedData = cache.get(cacheKey);
  
  if (cachedData) {
    yield put(fetchIssueInsightDetailAsync.success(cachedData));
    return;
  }
  
  try {
    const data = yield call(fetchIssueInsightDetails, filters);
    cache.set(cacheKey, data);
    yield put(fetchIssueInsightDetailAsync.success(data));
  } catch (error) {
    yield put(fetchIssueInsightDetailAsync.failure(error));
  }
}

Advanced Export Implementation

Export Queue Management

class ExportQueueManager {
  private queue: ExportTask[] = [];
  private processing = false;
  private maxConcurrent = 3;
  private activeExports = 0;
  
  async addExport(task: ExportTask) {
    this.queue.push(task);
    this.processQueue();
  }
  
  private async processQueue() {
    if (this.processing || this.activeExports >= this.maxConcurrent) {
      return;
    }
    
    this.processing = true;
    
    while (this.queue.length > 0 && this.activeExports < this.maxConcurrent) {
      const task = this.queue.shift();
      if (task) {
        this.activeExports++;
        this.executeExport(task).finally(() => {
          this.activeExports--;
          this.processQueue();
        });
      }
    }
    
    this.processing = false;
  }
  
  private async executeExport(task: ExportTask) {
    try {
      const data = await fetchExportData(task);
      await downloadFile(data, task.filename);
      task.onSuccess();
    } catch (error) {
      task.onError(error);
    }
  }
}

Custom Export Formats

interface ExportFormatter {
  format(data: any[], columns: string[]): string | Blob;
  mimeType: string;
  extension: string;
}
 
class CSVFormatter implements ExportFormatter {
  mimeType = 'text/csv';
  extension = 'csv';
  
  format(data: any[], columns: string[]): string {
    const headers = columns.join(',');
    const rows = data.map(row => 
      columns.map(col => this.escapeCSV(row[col])).join(',')
    );
    
    return [headers, ...rows].join('\n');
  }
  
  private escapeCSV(value: any): string {
    if (value === null || value === undefined) return '';
    const stringValue = String(value);
    if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
      return `"${stringValue.replace(/"/g, '""')}"`;
    }
    return stringValue;
  }
}
 
class ExcelFormatter implements ExportFormatter {
  mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  extension = 'xlsx';
  
  format(data: any[], columns: string[]): Blob {
    // Implementation using a library like xlsx
    const worksheet = XLSX.utils.json_to_sheet(data);
    const workbook = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(workbook, worksheet, 'Issue Insights');
    
    const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
    return new Blob([excelBuffer], { type: this.mimeType });
  }
}

Drill-Down State Management

Drill History Manager

interface DrillState {
  level: number;
  viewBy: string;
  groupBy: string;
  filters: any;
  breadcrumb: string;
}
 
class DrillDownHistoryManager {
  private history: DrillState[] = [];
  private maxDepth = 3;
  
  push(state: DrillState) {
    if (this.history.length >= this.maxDepth) {
      throw new Error('Maximum drill depth reached');
    }
    this.history.push(state);
  }
  
  pop(): DrillState | undefined {
    return this.history.pop();
  }
  
  reset() {
    this.history = [];
  }
  
  canDrillDown(): boolean {
    return this.history.length < this.maxDepth;
  }
  
  canDrillUp(): boolean {
    return this.history.length > 0;
  }
  
  getCurrentLevel(): number {
    return this.history.length;
  }
  
  getBreadcrumbs(): string[] {
    return this.history.map(state => state.breadcrumb);
  }
  
  getState(): DrillState[] {
    return [...this.history];
  }
}

Performance Optimization Techniques

Virtual Scrolling Implementation

const VirtualizedIssueTable = ({ data, rowHeight = 50 }) => {
  const [scrollTop, setScrollTop] = useState(0);
  const [containerHeight, setContainerHeight] = useState(600);
  const containerRef = useRef<HTMLDivElement>(null);
  
  const startIndex = Math.floor(scrollTop / rowHeight);
  const endIndex = Math.min(
    data.length - 1,
    Math.floor((scrollTop + containerHeight) / rowHeight)
  );
  
  const visibleItems = data.slice(startIndex, endIndex + 1);
  const totalHeight = data.length * rowHeight;
  const offsetY = startIndex * rowHeight;
  
  useEffect(() => {
    const handleResize = () => {
      if (containerRef.current) {
        setContainerHeight(containerRef.current.clientHeight);
      }
    };
    
    handleResize();
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
    setScrollTop(e.currentTarget.scrollTop);
  };
  
  return (
    <div
      ref={containerRef}
      style={{ height: '600px', overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <IssueRow
              key={startIndex + index}
              data={item}
              style={{ height: rowHeight }}
            />
          ))}
        </div>
      </div>
    </div>
  );
};

Debounced Filter Updates

const useDebouncdFilters = (filters: Filters, delay = 500) => {
  const [debouncedFilters, setDebouncedFilters] = useState(filters);
  const timeoutRef = useRef<NodeJS.Timeout>();
  
  useEffect(() => {
    timeoutRef.current = setTimeout(() => {
      setDebouncedFilters(filters);
    }, delay);
    
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, [filters, delay]);
  
  return debouncedFilters;
};

Logging and Monitoring

Structured Logging

class IssueInsightsLogger {
  private context = 'IssueInsights';
  
  info(message: string, data?: any) {
    console.log(JSON.stringify({
      level: 'info',
      context: this.context,
      message,
      data,
      timestamp: new Date().toISOString()
    }));
  }
  
  error(message: string, error: Error, data?: any) {
    console.error(JSON.stringify({
      level: 'error',
      context: this.context,
      message,
      error: {
        message: error.message,
        stack: error.stack
      },
      data,
      timestamp: new Date().toISOString()
    }));
  }
  
  metric(name: string, value: number, tags?: Record<string, string>) {
    // Send to metrics service
    metricsService.track({
      metric: name,
      value,
      tags: {
        ...tags,
        module: 'issue-insights'
      }
    });
  }
}

This documentation is maintained by the Nimbly Engineering Team. For questions or updates, please contact the team via GitHub issues.