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 Path | Component | Access Control | Description |
|---|---|---|---|
/analytics/operational/issue-insights | OperationalIssueInsightsDashboardPage | RoleResources.DASHBOARD_OVERVIEW_ALL | Main issue insights dashboard view |
/analytics/operational | OperationalDashboardPage | RoleResources.DASHBOARD_OVERVIEW_ALL | Parent operational dashboard |
/analytics/executive | ExecutiveDashboardPage | RoleResources.DASHBOARD_OVERVIEW_ALL | Executive level dashboard |
/issues | IssuesPage | Feature: ISSUE_TRACKER | Issue tracker listing page |
/issues/:issueId | IssuesDetailsPage | Feature: ISSUE_TRACKER | Individual issue details |
Navigation Flow
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]
Breadcrumb Structure
Desktop View:
- Dashboard → Operational KPI Dashboard → Issue Insights Dashboard
Mobile View:
- Operational KPI Dashboard → Issue Insights Dashboard
Deep Linking Support
The dashboard supports deep linking through query parameters for:
- Date ranges
- Site/Department/User filters
- Status filters
- Custom saved filters
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:
- Level 1: Overview by selected dimension
- Level 2: Breakdown by site/department
- 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 By | Available Columns |
|---|---|
| Site | Site Name, Issue Count, Resolution Time |
| Department | Department Name, Issue Count, Resolution Time |
| User | User Name, Email, Issue Count, Resolution Time |
| Category | Category Name, Issue Count, Resolution Time |
| Questionnaire | Questionnaire 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
| Endpoint | Method | Purpose | Parameters |
|---|---|---|---|
/statistics/dashboard/issue/detail | POST | Fetch issue insights summary | Dashboard filters |
/statistics/dashboard/issue | POST | Fetch issue list/chart data | filters, metric, viewBy, groupBy, limit, page |
/statistics/dashboard/issue/download/view | POST | Preview download data | groupBy, fieldName[], filters |
/statistics/dashboard/issue/download | POST | Download file (CSV/Excel) | groupBy, fieldName[], fileType, filters |
/issues/issues/bulkUpdate | PUT | Bulk update issues | issueIDs[], data, triggerNotification |
/issues/issueMessages/bulkCreate | POST | Bulk create issue messages | issueIDs[], data, triggerNotification |
/issues/issue-tracker-filters | GET/POST/PUT/DELETE | Manage custom filters | filterId (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 AuthContent-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:
- fetchIssueInsightDetailSaga: Fetches summary metrics
- fetchIssueInsightListSaga: Fetches list data for tables
- fetchIssueChartSaga: Fetches chart visualization data
- 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 dataFilters 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:
- User configures filters
- Clicks “Save Filters” button
- System calls
fetchSaveFiltersAPI - Filters stored in backend with user context
- Success toast notification
Load Filter Flow:
- Page loads
- System calls
fetchSavedFilters - If saved filters exist:
- Apply to Redux state
- Show notification modal
- Update UI components
- 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:
- Fetch parent dashboard (KPI) filters
- Clear all issue-specific filters
- Remove saved filter preferences
- Reset pagination to page 1
- 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
DashboardFilterswrapper - 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
| Shortcut | Action |
|---|---|
Ctrl/Cmd + F | Open filters |
Ctrl/Cmd + E | Export data |
Ctrl/Cmd + R | Refresh data |
Esc | Close modals |
Tab | Navigate 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
| Code | Description | Resolution |
|---|---|---|
| 400 | Invalid filter parameters | Check filter values |
| 401 | Authentication failed | Re-login required |
| 403 | Permission denied | Check user permissions |
| 404 | Data not found | Verify filter criteria |
| 429 | Rate limit exceeded | Wait and retry |
| 500 | Server error | Contact support |
Glossary
| Term | Definition |
|---|---|
| IRT | Issue Resolution Time - Time taken to resolve an issue |
| Primary Status | Main status of issue (Open, Resolved, Canceled) |
| Secondary Status | Sub-status providing more detail |
| Drill-down | Navigate to more detailed data view |
| Group By | Aggregate data by specified dimension |
| View By | Primary dimension for visualization |
References
External Documentation
Internal Resources
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2023-01 | Initial release |
| 1.1.0 | 2023-06 | Added drill-down navigation |
| 1.2.0 | 2023-09 | Filter persistence feature |
| 1.3.0 | 2024-01 | Performance optimizations |
| 1.4.0 | 2024-03 | Export 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 buildCode Standards
- Follow TypeScript best practices
- Write comprehensive tests
- Document complex logic
- Use meaningful variable names
- Keep components focused and small
Pull Request Process
- Create feature branch
- Implement changes
- Add/update tests
- Update documentation
- Submit PR with description
- 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.