TP-42467 | External Agency Dashboards V1: AM - Overall and Field Governance (#787)

* TP-42467 | Inital Commit

* TP-42467 | Table CHanges

* TP-42467 | Table Changes

* TP-42467 | Redux Setup + Actions Added

* TP-42467 | Table Changes

* TP-42467 | Routing Changes + Tabs Changes + Table Changes

* TP-42467 | Agent Name Options support added

* TP-42467 | Code Refactored

* TP-42467 | Column Def changes

* TP-42467 | API Contract Changes

* TP-42467 | API helper changes

* TP-42467 | Css Fixes + Agent Map Location Entry point added

* TP-42467 | Clickstream Added + Tab Issues Fixes + Feature Flags Added

* TP-42467 | Code Refactored

* TP-42467 | Mocks Added

* TP-42467 | Last Updated Logic Added

* TP-42467 | Polling Added

* TP-42461 |removed comments| Aman Singh

* TP-42467 |lint issues| Aman Singh

* TP-42467 | Table UI flickering fix

* TP-42467 | Filter changes for DPD Table

* TP-42467 | Filter changes + Query Params Changing + Refactoring + Routing

* TP-42467 | API Fix + Loading State Fix +  Table changes

* TP-42467 | Path Param Changes

* TP-42467 | API Integration

* TP-42467 | Code Refactored

* TP-42467 | Minor fix

* TP-42467 | Contract Changes + Code Refactored

* TP-42467 | Contract Changes

* TP-42467 | UAT Fixes

* TP-42467 | UAT Fixes

* TP-42467 | Time format fix

* TP-42467 | Bug Fixes

* TP-42467 | UAT Fixes

* TP-42467 | Code Refactored

* TP-42467 | Bug Fix

* TP-42467 | PR comments reolved

* TP-42467 | endpoint change

* TP-42567 | UAT Fix

* TP-42467 | web ui library update

---------

Co-authored-by: aman.singh <aman.singh@navi.com>
Co-authored-by: Varnit Goyal <varnit.goyal@navi.com>
This commit is contained in:
Mantri Ramkishor
2024-01-24 22:47:13 +05:30
committed by GitHub
parent ce8ed3e363
commit b76f96a567
52 changed files with 3294 additions and 52 deletions

View File

@@ -0,0 +1,26 @@
{
"agencies": [
{
"label": "All",
"value": "ALL"
},
{
"label": "Navi",
"value": "1000"
}
],
"states": [
{
"label": "All",
"value": "ALL"
},
{
"label": "Karnataka",
"value": "KA"
},
{
"label": "Gujarat",
"value": "GJ"
}
]
}

View File

@@ -0,0 +1,41 @@
{
"performanceData": {
"data": [
{
"agencyName": "AC1",
"agencyCode": "agency-code-1",
"coverageThirtyNinetyLansPercent": 1,
"coverageNinetyOneEightyLansPercent": 4.1212210,
"genuineCoverageThirtyNinetyLansPercent": 5.1321210,
"genuineCoverageNinetyOneEightyLansPercent": 2.121210,
"visitsPerDayPerAgent": 2.1321325,
"genuineVisitsPerDayPerAgent": 2.13213210,
"ptpsGenerated": 20,
"distanceTravelledPerDayPerAgent": 15.12321220,
"appUsagePerDayPerAgent": 3.132123210,
"brokenPtpPercentage": 5.123212320
},
{
"agencyName": "AC2",
"agencyCode": "agency-code-2",
"coverageThirtyNinetyLansPercent": null,
"coverageNinetyOneEightyLansPercent": 7.0,
"genuineCoverageThirtyNinetyLansPercent": 20.0,
"genuineCoverageNinetyOneEightyLansPercent": 5.0,
"visitsPerDayPerAgent": 7.5,
"genuineVisitsPerDayPerAgent": 2.0,
"ptpsGenerated": 14,
"distanceTravelledPerDayPerAgent": 30.0,
"appUsagePerDayPerAgent": 400.0,
"brokenPtpPercentage": 33.33
}
],
"pages": {
"pageNo": 0,
"totalPages": 1,
"pageSize": 10,
"totalElements": 2
}
},
"lastUpdatedAt": 1705336097367
}

View File

@@ -0,0 +1,8 @@
{
"agency": [
{
"label": "Agency Name",
"value": "AGENCY_CODE"
}
]
}

View File

@@ -0,0 +1,59 @@
{
"performanceData": {
"data": [
{
"agentName": "Pratham",
"agencyName": "TEst 1",
"agentReferenceId": "278d39c2-a2ae-4d69-91d8-515ec7907ee8",
"totalLansAllocated": null,
"coverageThirtyNinetyLansPercent": 5.123132,
"coverageNinetyOneEightyLansPercent": 2.123132,
"genuineCoverageThirtyNinetyLansPercent": 3.123132,
"genuineCoverageNinetyOneEightyLansPercent": 1.123132,
"visitsPerDay": 2.512322,
"genuineVisitsPerDay": 2.123132,
"ptpsGenerated": 1123132,
"distanceTravelledPerDay": 15.123132,
"appUsagePerDay": 3.123132,
"brokenPtpPercentage": 5.123132
},
{
"agentName": "Raghav",
"agentReferenceId": "780ec066-547f-4fac-82e8-1fd97c9c91d2",
"totalLansAllocated": null,
"coverageThirtyNinetyLansPercent": 50.0,
"coverageNinetyOneEightyLansPercent": 25.0,
"genuineCoverageThirtyNinetyLansPercent": 30.0,
"genuineCoverageNinetyOneEightyLansPercent": 15.0,
"visitsPerDay": 2.5,
"genuineVisitsPerDay": 2.0,
"ptpsGenerated": 10,
"distanceTravelledPerDay": 15.0,
"appUsagePerDay": 3.0,
"brokenPtpPercentage": 5.0
},
{
"agentName": "Shubham",
"agentReferenceId": "4075acc6-53c8-4204-9ad2-192313387d9b",
"totalLansAllocated": null,
"coverageThirtyNinetyLansPercent": 15.0,
"coverageNinetyOneEightyLansPercent": 7.0,
"genuineCoverageThirtyNinetyLansPercent": 20.0,
"genuineCoverageNinetyOneEightyLansPercent": 5.0,
"visitsPerDay": 7.5,
"genuineVisitsPerDay": 2.0,
"ptpsGenerated": 14,
"distanceTravelledPerDay": 30.0,
"appUsagePerDay": 400.0,
"brokenPtpPercentage": 33.33
}
],
"pages": {
"pageNo": 0,
"totalPages": 1,
"pageSize": 10,
"totalElements": 3
}
},
"lastUpdatedAt": 1705336097367
}

View File

@@ -0,0 +1,6 @@
[
{
"label": "Agent 1",
"value": "agent"
}
]

View File

@@ -1,14 +1,26 @@
{
"referenceId": "654615f9-8f88-46de-a1d8-707e87e82a2e",
"phoneNumber": "8632426173",
"name": "abcd",
"agencyCode": "TL369",
"agencyName": "AllocationDeallocation",
"active": true,
"roles": [
"ROLE_CALLING_AGENT"
],
"naviUser": false,
"agenciesReporting": [],
"email": "abcd@navi.com"
}
"referenceId": "654615f9-8f88-46de-a1d8-707e87e82a2e",
"phoneNumber": "8632426173",
"name": "abcd",
"agencyCode": "TL369",
"agencyName": "AllocationDeallocation",
"active": true,
"roles": [
"ROLE_CALLING_AGENT",
"ROLE_NAVI_FIELD_AGENCY_TEAM_LEAD",
"ROLE_NAVI_FIELD_TEAM_LEAD",
"ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD"
],
"naviUser": false,
"agenciesReporting": [],
"email": "abcd@navi.com",
"featureFlags": {
"performanceDashboard": true,
"externalAgencyPerformanceDashboard": true,
"overallPerformanceTab": true,
"fieldGovernanceTab": true,
"overallPerformanceOverallMetricsTable": true,
"fieldGovernanceAgencyPerformanceTable": true,
"fieldGovernanceAgentPerformanceTable": true
}
}

50
__mocks__/dpdBucket.json Normal file
View File

@@ -0,0 +1,50 @@
{
"performanceData": {
"data": [
{
"dpdBucket": "DPD 61-90",
"performanceBand": 3,
"lansAllocated": 934,
"agentAllocationPercentage": 1,
"emiOverdueAmount": 10609064,
"targetCEPercentage": 13.322222,
"targetOverdueAmount": 1424932.3232,
"emiCEPercentage": 2.222222,
"emiOverdueCollected": 244058,
"achievementPercentage": 10.3378,
"lmsdAchievedPercentage": 37.386836,
"nonEnachCashCollected": 280880
},
{
"dpdBucket": "DPD 61-90",
"performanceBand": 3,
"lansAllocated": 932,
"agentAllocationPercentage": 1,
"emiOverdueAmount": 10609064,
"targetCEPercentage": 13,
"targetOverdueAmount": 1424932,
"emiCEPercentage": 2,
"emiOverdueCollected": 244058,
"achievementPercentage": 0,
"lmsdAchievedPercentage": 37,
"nonEnachCashCollected": 280880
},
{
"dpdBucket": "DPD 61-90",
"performanceBand": 3,
"lansAllocated": 948,
"agentAllocationPercentage": 1,
"emiOverdueAmount": 10609064,
"targetCEPercentage": 13,
"targetOverdueAmount": 1424932,
"emiCEPercentage": 2,
"emiOverdueCollected": 244058,
"achievementPercentage": 0,
"lmsdAchievedPercentage": 37,
"nonEnachCashCollected": 280880
}
]
},
"lastUpdatedAt": 1705336097367
}

View File

@@ -76,6 +76,7 @@
--blue-base: #0276fe;
--blue-light: #3591fe;
--blue-dark: #025ecb;
--blue-light-table: #f7f8fc;
--blue-border: #cce4ff;
--blue-selected: #d1e5ff;
--blue-bg: #e6f1ff;
@@ -126,6 +127,8 @@
// Z-indices
--z-index-pagination-wrapper: 1;
--z-index-external-sensei-toggle-tabs: 2;
--z-index-external-sensei-header: 3;
--z-index-dropdown-picker: 10;
--z-index-side-logout-btn: 42;
--z-index-ameyo-disconnect-btn: 99;

View File

@@ -3,7 +3,6 @@
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
line-height: 24px;
width: fit-content;
cursor: text;
}

View File

@@ -2,6 +2,10 @@
position: relative;
display: inline-block;
input {
cursor: auto;
}
& .filter {
display: flex;
align-items: center;

View File

@@ -8,27 +8,39 @@ interface IRenderWithToolTip {
showToolTip?: boolean;
lineClamp?: number;
hiddenPadding?: number;
ellipsisWrapperClass?: string;
}
const TableCellRenderer: React.FC<IRenderWithToolTip> = props => {
const { value, toolTipContent, showToolTip, lineClamp = 1, hiddenPadding = 10 } = props;
const {
value,
toolTipContent,
showToolTip,
lineClamp = 1,
hiddenPadding = 10,
ellipsisWrapperClass
} = props;
if (!showToolTip) {
return <EllipsisText text={value ?? '-'} lineClamp={lineClamp} />;
return (
<EllipsisText
text={value ?? '-'}
lineClamp={lineClamp}
ellipsisWrapperClass={ellipsisWrapperClass}
/>
);
}
return (
<Tooltip placement="top" hiddenPadding={hiddenPadding} hideStrategy="referenceHidden">
<TooltipTrigger tooltipTriggerClassName="tooltipTriggerWrapper">
<EllipsisText text={value ?? '-'} lineClamp={lineClamp} />
<EllipsisText
text={value ?? '-'}
lineClamp={lineClamp}
ellipsisWrapperClass={ellipsisWrapperClass}
/>
</TooltipTrigger>
<TooltipContent
className="tooltipWrapper"
arrowColor="var(--tooltip-background-color)"
bgColor="var(--tooltip-background-color)"
>
{toolTipContent}
</TooltipContent>
<TooltipContent className="tooltipWrapper">{toolTipContent}</TooltipContent>
</Tooltip>
);
};

View File

@@ -74,12 +74,14 @@ function SideNavBar({ isDc97User, isHRCChatUser }: ISideNavbarProps) {
isInternalTeamLead,
fieldTLAndClusterAndZonalManagers,
isAmeyoUtilityVisible,
isAmeyoGeneratePasswordVisible
isAmeyoGeneratePasswordVisible,
featureFlags
} = useSelector((state: RootState) => ({
user: state.common.userData,
isPerformanceDashboardVisible: state?.common?.isPerformanceDashboardVisible,
isIdCardApprovalVisible: state?.common?.isIdCardApprovalVisible,
isPincodeMappingVisible: state?.common?.isPincodeMappingVisible,
featureFlags: state?.common?.featureFlags,
isTeamLead: state?.common?.isTeamLead,
isInternalTeamLead: state?.common?.isInternalTeamLead,
fieldTLAndClusterAndZonalManagers: state?.common?.fieldTLAndClusterAndZonalManagers,
@@ -87,6 +89,8 @@ function SideNavBar({ isDc97User, isHRCChatUser }: ISideNavbarProps) {
isAmeyoGeneratePasswordVisible: state?.common?.isAmeyoGeneratePasswordVisible
}));
const { externalAgencyPerformanceDashboard } = featureFlags || {};
const newDashboard = user?.revampedDashboardViewAllowed;
const {
isCallConnected,
@@ -382,7 +386,11 @@ function SideNavBar({ isDc97User, isHRCChatUser }: ISideNavbarProps) {
activeIcon={<PDIcon />}
inActiveIcon={<PDIcon fillColor="var(--navigation-blue-c2)" />}
name={'Dashboard'}
route={APP_ROUTES.PERFORMANCE_DASHBOARD.path}
route={
externalAgencyPerformanceDashboard
? APP_ROUTES.SENSEI_EXTERNAL.relativePath
: APP_ROUTES.PERFORMANCE_DASHBOARD.path
}
linkClickHandler={linkClickHandler}
currentPathname={pathname}
currentSearch={search}

View File

@@ -9,19 +9,24 @@ interface ToggleTabsProps {
onTabClick: (val: string | boolean) => void;
tabOptions: ITabsOption[];
size?: FontType;
containerClass?: string;
activeTabClass?: string;
}
const ToggleTabs: React.FC<ToggleTabsProps> = props => {
const { selected, onTabClick, tabOptions, size } = props;
const { selected, onTabClick, tabOptions, size, containerClass, activeTabClass } = props;
return (
<div className={styles.container}>
<div className={cx(styles.container, containerClass)}>
<div className={styles.feedbackTabsWrapper}>
{tabOptions.map((item, index) => (
<div
key={index}
onClick={() => onTabClick(item.value)}
className={cx(styles.tabItem, selected === item.value ? styles.active : '')}
className={cx(styles.tabItem, {
[styles.active]: selected === item.value,
[`${activeTabClass}`]: selected === item.value
})}
>
<Typography variant={size ? size : 'h4'}>{item.label}</Typography>
</div>

View File

@@ -97,7 +97,10 @@ const HideTopNavBarRoutes = [
APP_ROUTES.AGENCY_PINCODE_MAPPING.path,
APP_ROUTES.WHATSAPP_CHATBOT.path,
APP_ROUTES.SENSEI_TELE.path,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSAI.daily_planning,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.daily_planning,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.overall_performance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.field_governance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.external_daily_planning,
APP_ROUTES.AMEYO_UTILITY.path,
APP_ROUTES.GENERATE_AMEYO_PASSWORD.path,
APP_ROUTES.AMEYO_UPLOAD_NUMBERS.path

View File

@@ -19,10 +19,14 @@ import SenseiTele from '../pages/SenseiTele';
const Cases = React.lazy(() => import('../pages/Cases/components/Cases'));
const NotFound = React.lazy(() => import('../pages/404/NotFound'));
const ExternalDashboardSensei = React.lazy(() => import('../pages/ExternalDashboardSensei'));
export const APP_ROUTES_PATHS_WITH_PATH_PARAM = {
SENSAI: {
daily_planning: '/sensei/daily_planning'
SENSEI: {
daily_planning: '/sensei/daily_planning',
overall_performance: '/sensei-external/overall_performance',
field_governance: '/sensei-external/field_governance',
external_daily_planning: '/sensei-external/daily_planning'
}
};
@@ -66,6 +70,11 @@ const APP_ROUTES = {
path: '/live-location-tracker',
element: <LiveLocationTracker />
},
SENSEI_EXTERNAL: {
path: '/sensei-external/:tabId',
relativePath: APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.overall_performance,
element: <ExternalDashboardSensei />
},
REALLOCATION: {
path: '/reallocation',
element: <Reallocation />,
@@ -73,7 +82,7 @@ const APP_ROUTES = {
},
SENSEI: {
path: '/sensei/:tabId',
relativePath: APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSAI.daily_planning,
relativePath: APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.daily_planning,
element: <Sensei />
},
AGENT_AVAILABILITY: {

View File

@@ -0,0 +1,52 @@
import { RootState } from '@cp/src/store';
import { readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { useSelector } from 'react-redux';
import AgencyDetailsTableMtd from '../components/AgencyDetailsTableExternal/AgencyDetailsTableMtd';
import AgencyDetailsTableToday from '../components/AgencyDetailsTableExternal/AgencyDetailsTableToday';
import { FIELD_GOVERNANCE_TABS_VALUE } from '../types';
import cx from 'classnames';
import styles from './fieldGovernance.module.scss';
import { TabsKey } from '../constants';
import { useParams } from 'react-router-dom';
const AgencyDetailsTable = () => {
const params = readQueryParams();
const { featureFlags } = useSelector((state: RootState) => ({
featureFlags: state?.common?.featureFlags
}));
const { fieldGovernanceAgencyPerformanceTable } = featureFlags || {};
const selectedTab = params?.fieldGovernanceTab?.selectedTab;
const { tabId = '' } = useParams();
const isToday = selectedTab === FIELD_GOVERNANCE_TABS_VALUE.TODAY;
const isFieldGovernance = tabId === TabsKey.FIELD_GOVERNANCE;
const isMtdTableVisible = !isToday && isFieldGovernance && !!selectedTab;
const isTodayTableVisible = isToday && isFieldGovernance && !!selectedTab;
if (fieldGovernanceAgencyPerformanceTable) {
return (
<div>
<div
className={cx({
[styles.tableVisible]: isMtdTableVisible,
[styles.tableHidden]: isTodayTableVisible
})}
>
<AgencyDetailsTableMtd isVisible={isMtdTableVisible} />
</div>
<div
className={cx({
[styles.tableVisible]: isTodayTableVisible,
[styles.tableHidden]: isMtdTableVisible
})}
>
<AgencyDetailsTableToday isVisible={isTodayTableVisible} />
</div>
</div>
);
}
return null;
};
export default AgencyDetailsTable;

View File

@@ -0,0 +1,63 @@
import { RootState } from '@cp/src/store';
import { readQueryParams } from '@cp/src/utils/QueryParamsHelper';
import { useDispatch, useSelector } from 'react-redux';
import AgentPerformanceTableMtd from '../components/AgentPerformanceTableExternal/AgentPerformanceTableMtd';
import AgentPerformanceTableToday from '../components/AgentPerformanceTableExternal/AgentPerformanceTableToday';
import { FIELD_GOVERNANCE_TABS_VALUE } from '../types';
import cx from 'classnames';
import styles from './fieldGovernance.module.scss';
import { TabsKey } from '../constants';
import { useParams } from 'react-router-dom';
import { getAgencyOptions } from '../actions/ExternalDashboardSenseiActions';
import { useEffect } from 'react';
const AgentPerformanceTable = () => {
const params = readQueryParams();
const { featureFlags } = useSelector((state: RootState) => ({
featureFlags: state?.common?.featureFlags
}));
const { fieldGovernanceAgentPerformanceTable } = featureFlags || {};
const selectedTab = params?.fieldGovernanceTab?.selectedTab;
const { tabId = '' } = useParams();
const dispatch = useDispatch();
const isToday = selectedTab === FIELD_GOVERNANCE_TABS_VALUE.TODAY;
const isFieldGovernance = tabId === TabsKey.FIELD_GOVERNANCE;
const isMtdTableVisible = !isToday && isFieldGovernance && !!selectedTab;
const isTodayTableVisible = isToday && isFieldGovernance && !!selectedTab;
useEffect(() => {
if (fieldGovernanceAgentPerformanceTable && isFieldGovernance) {
dispatch(getAgencyOptions());
}
}, [isFieldGovernance]);
if (fieldGovernanceAgentPerformanceTable) {
return (
<>
<div
className={cx({
[styles.tableVisible]: isMtdTableVisible,
[styles.tableHidden]: isTodayTableVisible
})}
>
<AgentPerformanceTableMtd isVisible={isMtdTableVisible} />
</div>
<div
className={cx({
[styles.tableVisible]: isTodayTableVisible,
[styles.tableHidden]: isMtdTableVisible
})}
>
<AgentPerformanceTableToday isVisible={isTodayTableVisible} />
</div>
</>
);
}
return null;
};
export default AgentPerformanceTable;

View File

@@ -0,0 +1,26 @@
.governanceTabWrapper {
position: relative;
}
.toggleTabsContainer {
padding-top: 16px;
z-index: var(--z-index-external-sensei-toggle-tabs);
}
.activeTab {
box-shadow: var(--box-shadow-2);
}
.tableVisible {
visibility: visible;
position: inherit;
width: 100%;
}
.tableHidden {
visibility: hidden;
position: absolute;
width: 100%;
top: 0;
left: 0;
}

View File

@@ -0,0 +1,82 @@
import { useNavigate, useParams } from 'react-router-dom';
import { createQueryParams, readQueryParams } from 'src/utils/QueryParamsHelper';
import styles from './fieldGovernance.module.scss';
import { useEffect, useRef } from 'react';
import { FIELD_GOVERNANCE_TABS, FIELD_GOVERNANCE_TABS_VALUE } from '../types';
import ToggleTabs from 'src/components/toggleTabs/ToggleTabs';
import { QueryParamTypeMapping, TabsKey } from '../constants';
import { setInitialGovernanceTabParams } from '../utils';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@cp/src/service/clickStream.constant';
import AgencyDetailsTable from './AgencyDetailsTable';
import AgentPerformanceTable from './AgentPerformanceTable';
import { useSelector } from 'react-redux';
import { RootState } from '@cp/src/store';
const FieldGovernance = () => {
const navigate = useNavigate();
const params = readQueryParams();
const { featureFlags } = useSelector((state: RootState) => ({
featureFlags: state?.common?.featureFlags
}));
const selectedTab = params?.fieldGovernanceTab?.selectedTab;
const { tabId = '' } = useParams();
const governanceTabRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (tabId === TabsKey.FIELD_GOVERNANCE) {
setInitialGovernanceTabParams(params, navigate, featureFlags);
governanceTabRef.current?.scrollIntoView({
behavior: 'smooth'
});
}
}, [tabId]);
const onTabClickHandler = (val: string | boolean) => {
const queryParams = { ...params };
// Reseting Params - Page Details
if (queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE]) {
queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE].pageNumber = '1';
queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE].pageSize = '10';
}
if (queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE]) {
queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE].pageNumber = '1';
queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE].pageSize = '10';
}
queryParams.fieldGovernanceTab.selectedTab = val;
const updatedParams = createQueryParams(queryParams);
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_GOVERNANCE_TAB_SWITCH, {
currentTab: selectedTab,
newTab: val
});
navigate(updatedParams);
};
return (
<div className={styles.governanceTabWrapper} ref={governanceTabRef}>
<ToggleTabs
containerClass={styles.toggleTabsContainer}
activeTabClass={styles.activeTab}
selected={selectedTab}
tabOptions={[
{
label: FIELD_GOVERNANCE_TABS.MTD,
value: FIELD_GOVERNANCE_TABS_VALUE.MTD
},
{
label: FIELD_GOVERNANCE_TABS.TODAY,
value: FIELD_GOVERNANCE_TABS_VALUE.TODAY
}
]}
onTabClick={val => onTabClickHandler(val)}
/>
<AgencyDetailsTable />
<AgentPerformanceTable />
</div>
);
};
export default FieldGovernance;

View File

@@ -0,0 +1,34 @@
import { RootState } from '@cp/src/store';
import { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import DpdBucketTable from '../components/DpdBucketTableExternal';
import { TabsKey } from '../constants';
const OverallPerformance = () => {
const overallTabRef = useRef<HTMLDivElement>(null);
const { featureFlags } = useSelector((state: RootState) => ({
featureFlags: state?.common?.featureFlags
}));
const { overallPerformanceOverallMetricsTable } = featureFlags || {};
const { tabId = '' } = useParams();
const isOverallPerformance = tabId === TabsKey.OVERALL_PERFORMANCE;
useEffect(() => {
if (isOverallPerformance) {
overallTabRef.current?.scrollIntoView({
behavior: 'smooth'
});
}
}, [tabId]);
return (
<div ref={overallTabRef}>
{overallPerformanceOverallMetricsTable && <DpdBucketTable isVisible={isOverallPerformance} />}
</div>
);
};
export default OverallPerformance;

View File

@@ -0,0 +1,192 @@
import { Dispatch } from '@reduxjs/toolkit';
import axiosInstance, { ApiKeys, API_STATUS_CODE, getApiUrl, logError } from 'src/utils/ApiHelper';
import {
setAgencyDetailsTable,
setAgencyNameOptions,
setAgentPerformanceTable,
setAgentPerformanceTlTable,
setAgentsOptions,
setDpdBucketTable,
setLoadingAgencyDetails,
setLoadingAgentPerformance,
setLoadingAgentPerformanceTl,
setLoadingDpdBucket,
setStateOptions,
setTablesLastUpdatedAt
} from '../reducers/ExternalDashboardSenseiSlice';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import { FIELD_GOVERNANCE_TABS_VALUE } from '../types';
export const getDpdBucketData = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_DPD_BUCKET_DATA);
const params = readQueryParams();
dispatch(setLoadingDpdBucket(true));
const { agencyCode = 'ALL', stateName = 'ALL' } = params?.dpdBucketTable || {};
axiosInstance
.get(url, {
params: {
stateName,
agencyCode
}
})
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
const { performanceData, lastUpdatedAt } = response?.data || {};
dispatch(setDpdBucketTable({ data: performanceData }));
dispatch(
setTablesLastUpdatedAt({
dpdBucketLastUpdatedAt: lastUpdatedAt
})
);
}
})
.catch(err => {
dispatch(setDpdBucketTable({ data: [] }));
logError(err);
})
.finally(() => {
dispatch(setLoadingDpdBucket(false));
});
};
export const getAgencyAndStateOptions = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENCY_NAME_AND_STATE_OPTIONS);
const params = readQueryParams();
const { stateName = 'ALL' } = params?.dpdBucketTable || {};
axiosInstance
.get(url, {
params: {
stateName
}
})
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
dispatch(setAgencyNameOptions(response?.data?.agencies));
dispatch(setStateOptions(response?.data?.states));
}
})
.catch(err => {
logError(err);
});
};
export const getAgencyOptions = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENCY_NAME_OPTIONS);
axiosInstance
.get(url)
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
const { agencies = [] } = response?.data || {};
dispatch(setAgentsOptions(agencies));
}
})
.catch(err => {
logError(err);
});
};
export const getAgencyDetailsData = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENCY_DETAILS_DATA);
const params = readQueryParams();
dispatch(setLoadingAgencyDetails(true));
const { pageSize = '10', pageNumber = '1' } = params?.agencyDetailsTable || {};
const { selectedTab } = params?.fieldGovernanceTab || {};
const isToday = selectedTab === FIELD_GOVERNANCE_TABS_VALUE.TODAY;
axiosInstance
.get(url, {
params: {
pageSize,
pageNo: pageNumber - 1, // backend pagination starts from 0 so we are subtracting 1
viewType: selectedTab
}
})
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
const { performanceData, lastUpdatedAt } = response?.data || {};
dispatch(setAgencyDetailsTable({ data: performanceData, isToday }));
dispatch(
setTablesLastUpdatedAt({
agencyDetailsLastUpdatedAt: lastUpdatedAt
})
);
}
})
.catch(err => {
dispatch(setAgencyDetailsTable({ data: [] }));
logError(err);
})
.finally(() => {
dispatch(setLoadingAgencyDetails(false));
});
};
export const getAgentPerformanceData = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENT_PERFORMANCE_DATA);
const params = readQueryParams();
dispatch(setLoadingAgentPerformance(true));
const { agencyCode, pageSize = '10', pageNumber = '1' } = params?.agentPerformanceTable || {};
const { selectedTab } = params?.fieldGovernanceTab || {};
const isToday = selectedTab === FIELD_GOVERNANCE_TABS_VALUE.TODAY;
axiosInstance
.get(url, {
params: {
agencyCode,
pageSize,
pageNo: pageNumber - 1, // backend pagination starts from 0 so we are subtracting 1
viewType: selectedTab
}
})
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
const { performanceData, lastUpdatedAt } = response?.data || {};
dispatch(setAgentPerformanceTable({ data: performanceData, isToday }));
dispatch(
setTablesLastUpdatedAt({
agentPerformanceLastUpdatedAt: lastUpdatedAt
})
);
}
})
.catch(err => {
dispatch(setAgentPerformanceTable({ data: [] }));
logError(err);
})
.finally(() => {
dispatch(setLoadingAgentPerformance(false));
});
};
export const getAgentPerformanceTLData = () => (dispatch: Dispatch) => {
const url = getApiUrl(ApiKeys.GET_AGENT_PERFORMANCE_TL_DATA);
const params = readQueryParams();
dispatch(setLoadingAgentPerformanceTl(true));
const { dpdBucket = 'ALL' } = params?.agentPerformanceTlTable || {};
axiosInstance
.get(url, {
params: {
dpdBucket
}
})
.then(response => {
if (response.status === API_STATUS_CODE.OK) {
const { performanceData, lastUpdatedAt } = response?.data || {};
dispatch(setAgentPerformanceTlTable({ data: performanceData }));
dispatch(
setTablesLastUpdatedAt({
agentPerformanceTlLastUpdatedAt: lastUpdatedAt
})
);
}
})
.catch(err => {
dispatch(setAgentPerformanceTlTable({ data: [] }));
logError(err);
})
.finally(() => {
dispatch(setLoadingAgentPerformanceTl(false));
});
};

View File

@@ -0,0 +1,76 @@
import Loader from 'src/components/Loader/Loader';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import RenderAgTable from '../RenderAgTable';
import styles from './styles.module.scss';
import commonStyles from '../commonStyles.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'src/store';
import { getAgencyDetailsData } from '../../actions/ExternalDashboardSenseiActions';
import { AGENCY_DETAILS_COLUMNS } from '../ColumnDefs';
import {
FIELD_GOVERNANCE_TABLE_HEIGHT,
POLLING_TIME,
QueryParamTypeMapping
} from '../../constants';
import cx from 'classnames';
import React, { useEffect } from 'react';
import { poll } from '@cp/src/utils/polling';
import { noop } from '@navi/web-ui/lib/utils/common';
interface IAgencyDetailsTableMtd {
isVisible: boolean;
}
const AgencyDetailsTableMtd: React.FC<IAgencyDetailsTableMtd> = props => {
const { isVisible } = props;
const params = readQueryParams();
const { agencyDetailsTable } = useSelector((store: RootState) => ({
agencyDetailsTable: store?.externalDashboardSensei?.agencyDetailsTable
}));
const unsubscribe = React.useRef<() => void>();
const { agencyDetailsMtdData, loading } = agencyDetailsTable || {};
const { data = [], pages } = agencyDetailsMtdData || {};
const totalElements = pages?.totalElements || 0;
const currentPage = Number(params?.agencyDetailsTable?.pageNumber) || 1;
const pageSize = Number(params?.agencyDetailsTable?.pageSize) || 10;
const dispatch = useDispatch();
useEffect(() => {
if (isVisible) {
unsubscribe.current = poll(
() => dispatch(getAgencyDetailsData()),
json => json,
POLLING_TIME,
noop
);
}
return () => {
unsubscribe.current?.();
};
}, [isVisible]);
return (
<div className={cx(styles.agencyDetailsContainer, { [styles.noData]: !totalElements })}>
<div className="stickyHeader">
<div className={commonStyles.tableHeaderTitle}>Agency details</div>
</div>
<RenderAgTable
data={data}
colDefs={AGENCY_DETAILS_COLUMNS}
pageDetails={{ totalElements, currentPage, pageSize }}
tableName={QueryParamTypeMapping.AGENCY_DETAILS_TABLE}
callBackFn={() => dispatch(getAgencyDetailsData())}
tableHeight={FIELD_GOVERNANCE_TABLE_HEIGHT}
/>
<Loader
show={loading}
className={cx(commonStyles.loadingState, { [commonStyles.paginationLoading]: pageSize })}
animate={false}
/>
</div>
);
};
export default AgencyDetailsTableMtd;

View File

@@ -0,0 +1,77 @@
import Loader from 'src/components/Loader/Loader';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import RenderAgTable from '../RenderAgTable';
import styles from './styles.module.scss';
import commonStyles from '../commonStyles.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { getAgencyDetailsData } from '../../actions/ExternalDashboardSenseiActions';
import { TODAY_AGENCY_DETAILS_COLUMNS } from '../ColumnDefs';
import { RootState } from '@cp/src/store';
import {
FIELD_GOVERNANCE_TABLE_HEIGHT,
POLLING_TIME,
QueryParamTypeMapping
} from '../../constants';
import React, { useEffect, useRef } from 'react';
import { poll } from '@cp/src/utils/polling';
import { noop } from '@navi/web-ui/lib/utils/common';
import cx from 'classnames';
interface IAgencyDetailsTableToday {
isVisible: boolean;
}
const AgencyDetailsTableToday: React.FC<IAgencyDetailsTableToday> = props => {
const { isVisible } = props;
const params = readQueryParams();
const { agencyDetailsTable } = useSelector((store: RootState) => ({
agencyDetailsTable: store?.externalDashboardSensei?.agencyDetailsTable
}));
const { agencyDetailsTodayData, loading } = agencyDetailsTable || {};
const { data = [], pages } = agencyDetailsTodayData || {};
const unsubscribe = useRef<() => void>();
const dispatch = useDispatch();
const totalElements = pages?.totalElements || 0;
const currentPage = Number(params?.agencyDetailsTable?.pageNumber) || 1;
const pageSize = Number(params?.agencyDetailsTable?.pageSize) || 10;
useEffect(() => {
if (isVisible) {
unsubscribe.current = poll(
() => dispatch(getAgencyDetailsData()),
json => json,
POLLING_TIME,
noop
);
}
return () => {
unsubscribe.current?.();
};
}, [isVisible]);
return (
<div className={styles.agencyDetailsContainer}>
<div className="stickyHeader">
<div className={commonStyles.tableHeaderTitle}>Agency details</div>
</div>
<RenderAgTable
data={data}
colDefs={TODAY_AGENCY_DETAILS_COLUMNS}
pageDetails={{ totalElements, currentPage, pageSize }}
tableName={QueryParamTypeMapping.AGENCY_DETAILS_TABLE}
callBackFn={() => dispatch(getAgencyDetailsData())}
tableHeight={FIELD_GOVERNANCE_TABLE_HEIGHT}
/>
<Loader
show={loading}
className={cx(commonStyles.loadingState, { [commonStyles.paginationLoading]: pageSize })}
animate={false}
/>
</div>
);
};
export default AgencyDetailsTableToday;

View File

@@ -0,0 +1,8 @@
.agencyDetailsContainer {
position: relative;
margin-bottom: 90px;
}
.noData {
margin-bottom: 20px;
}

View File

@@ -0,0 +1,92 @@
import Loader from 'src/components/Loader/Loader';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import styles from './styles.module.scss';
import RenderAgTable from '../RenderAgTable';
import FilterWrapper from '../FilterWrapper';
import commonStyles from '../commonStyles.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'src/store';
import { getAgentPerformanceData } from '../../actions/ExternalDashboardSenseiActions';
import { AGENCY_PERFORMANCE_COLUMNS } from '../ColumnDefs';
import {
FIELD_GOVERNANCE_TABLE_HEIGHT,
filterTitle,
filterTypeMapping,
POLLING_TIME,
QueryParamTypeMapping
} from '../../constants';
import React, { useEffect, useRef } from 'react';
import { poll } from '@cp/src/utils/polling';
import { noop } from '@navi/web-ui/lib/utils/common';
import cx from 'classnames';
interface IAgentPerformanceTableMtd {
isVisible: boolean;
}
const AgentPerformanceTableMtd: React.FC<IAgentPerformanceTableMtd> = props => {
const { isVisible } = props;
const params = readQueryParams();
const { agentPerformanceTable, agentOptions } = useSelector((store: RootState) => ({
agentPerformanceTable: store?.externalDashboardSensei?.agentPerformanceTable,
agentOptions: store?.externalDashboardSensei?.agentsOptions
}));
const { agentPerformanceMtdData, loading } = agentPerformanceTable || {};
const { data = [], pages } = agentPerformanceMtdData || {};
const unsubscribe = useRef<() => void>();
const totalElements = pages?.totalElements || 0;
const currentPage = Number(params?.agentPerformanceTable?.pageNumber) || 1;
const pageSize = Number(params?.agentPerformanceTable?.pageSize) || 10;
const dispatch = useDispatch();
useEffect(() => {
if (isVisible) {
unsubscribe.current = poll(
() => dispatch(getAgentPerformanceData()),
json => json,
POLLING_TIME,
noop
);
}
return () => {
unsubscribe.current?.();
};
}, [isVisible]);
return (
<div className={styles.agentPerformanceContainer}>
<div className={cx(styles.tableHeader, 'stickyHeader')}>
<div className={commonStyles.tableHeaderTitle}>Agent Performance</div>
<FilterWrapper
title={filterTitle.agency}
options={agentOptions}
filterType={filterTypeMapping.agencyCode}
tableType={QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE}
callBackFn={() => {
dispatch(getAgentPerformanceData());
}}
isPaginatedTable
disabled={loading}
/>
</div>
<RenderAgTable
data={data}
colDefs={AGENCY_PERFORMANCE_COLUMNS}
pageDetails={{ totalElements, currentPage, pageSize }}
tableName={QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE}
callBackFn={() => dispatch(getAgentPerformanceData())}
tableHeight={FIELD_GOVERNANCE_TABLE_HEIGHT}
/>
<Loader
show={loading}
className={cx(commonStyles.loadingState, { [commonStyles.paginationLoading]: pageSize })}
animate={false}
/>
</div>
);
};
export default AgentPerformanceTableMtd;

View File

@@ -0,0 +1,92 @@
import Loader from 'src/components/Loader/Loader';
import { readQueryParams } from 'src/utils/QueryParamsHelper';
import styles from './styles.module.scss';
import RenderAgTable from '../RenderAgTable';
import FilterWrapper from '../FilterWrapper';
import commonStyles from '../commonStyles.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { getAgentPerformanceData } from '../../actions/ExternalDashboardSenseiActions';
import { TODAY_AGENCY_PERFORMANCE_COLUMNS } from '../ColumnDefs';
import { RootState } from '@cp/src/store';
import {
FIELD_GOVERNANCE_TABLE_HEIGHT,
filterTitle,
filterTypeMapping,
POLLING_TIME,
QueryParamTypeMapping
} from '../../constants';
import React, { useEffect, useRef } from 'react';
import { poll } from '@cp/src/utils/polling';
import { noop } from '@navi/web-ui/lib/utils/common';
import cx from 'classnames';
interface IAgentPerformanceTableToday {
isVisible: boolean;
}
const AgentPerformanceTableToday: React.FC<IAgentPerformanceTableToday> = props => {
const { isVisible } = props;
const params = readQueryParams();
const { agentPerformanceTable, agentOptions } = useSelector((store: RootState) => ({
agentPerformanceTable: store?.externalDashboardSensei?.agentPerformanceTable,
agentOptions: store?.externalDashboardSensei?.agentsOptions
}));
const unsubscribe = useRef<() => void>();
const { agentPerformanceTodayData, loading } = agentPerformanceTable || {};
const { data = [], pages } = agentPerformanceTodayData || {};
const dispatch = useDispatch();
const totalElements = pages?.totalElements || 0;
const currentPage = Number(params?.agentPerformanceTable?.pageNumber) || 1;
const pageSize = Number(params?.agentPerformanceTable?.pageSize) || 10;
useEffect(() => {
if (isVisible) {
unsubscribe.current = poll(
() => dispatch(getAgentPerformanceData()),
json => json,
POLLING_TIME,
noop
);
}
return () => {
unsubscribe.current?.();
};
}, [isVisible]);
return (
<div className={styles.agentPerformanceContainer}>
<div className={cx(styles.tableHeader, 'stickyHeader')}>
<div className={commonStyles.tableHeaderTitle}>Agent Performance</div>
<FilterWrapper
title={filterTitle.agency}
options={agentOptions}
filterType={filterTypeMapping.agencyCode}
tableType={QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE}
callBackFn={() => {
dispatch(getAgentPerformanceData());
}}
isPaginatedTable
disabled={loading}
/>
</div>
<RenderAgTable
data={data}
colDefs={TODAY_AGENCY_PERFORMANCE_COLUMNS}
pageDetails={{ totalElements, currentPage, pageSize }}
tableName={QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE}
callBackFn={() => dispatch(getAgentPerformanceData())}
tableHeight={FIELD_GOVERNANCE_TABLE_HEIGHT}
/>
<Loader
show={loading}
className={cx(commonStyles.loadingState, { [commonStyles.paginationLoading]: pageSize })}
animate={false}
/>
</div>
);
};
export default AgentPerformanceTableToday;

View File

@@ -0,0 +1,8 @@
.agentPerformanceContainer {
position: relative;
margin-bottom: 92px;
}
.tableHeader {
padding-bottom: 8px;
}

View File

@@ -0,0 +1,886 @@
import TableCellRenderer from '@cp/src/components/TableCellRenderer';
import CellRendererShortNumber from '@cp/src/components/TableCellRenderer/CellRendererShortNumber';
import { ColDefsType } from '@navi/web-ui/lib/components/AgTable/types';
import { GridReadyEvent, ICellRendererParams } from 'ag-grid-community';
import { formatNumber, pluralisation, toFixedValueNotation } from 'src/utils/commonUtils';
export const handleGridReady = (event: GridReadyEvent<any>, gridRef: any) => {
gridRef.current = event;
event.api.sizeColumnsToFit();
};
const secondsToHM = (seconds: number) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (!hours && !minutes) return '0 min';
return `${hours > 0 ? pluralisation(`${hours} hr`, hours, 's') : ''} ${
minutes > 0 ? pluralisation(`${minutes} min`, minutes, 's') : ''
}`;
};
export const DPD_BUCKET_COLUMNS = [
{
headerComponent: () => <>DPD bucket</>,
field: 'dpdBucket',
width: 120,
cellRenderer: (params: ICellRendererParams) => {
const { dpdBucket } = params.data || {};
return <TableCellRenderer value={dpdBucket} />;
}
},
{
headerComponent: () => <>Performance band</>,
field: 'performanceBand',
width: 140,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { performanceBand } = params?.data || {};
return <TableCellRenderer value={performanceBand ?? ' '} />;
}
},
{
headerComponent: () => <>LANs allocated</>,
field: 'lansAllocated',
width: 110,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { lansAllocated } = params?.data || {};
const parsedValue = formatNumber(lansAllocated ?? 0);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => <>Agent allocation</>,
field: 'agentAllocationPercentage',
width: 120,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { agentAllocationPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(agentAllocationPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => <>EMI OD</>,
field: 'emiOverdueAmount',
width: 100,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiOverdueAmount } = params?.data || {};
return <CellRendererShortNumber value={emiOverdueAmount} isCurrency significantDigits={2} />;
}
},
{
headerComponent: () => <>Target CE</>,
field: 'targetCEPercentage',
width: 90,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { targetCEPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(targetCEPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Target OD <br /> amount
</>
),
field: 'targetOverdueAmount',
width: 115,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { targetOverdueAmount } = params?.data || {};
return (
<CellRendererShortNumber value={targetOverdueAmount} isCurrency significantDigits={2} />
);
}
},
{
headerComponent: () => <>EMI CE</>,
field: 'emiCEPercentage',
width: 80,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiCEPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(emiCEPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
EMI OD <br /> collected
</>
),
field: 'emiOverdueCollected',
width: 110,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiOverdueCollected } = params?.data || {};
return (
<CellRendererShortNumber value={emiOverdueCollected} isCurrency significantDigits={2} />
);
}
},
{
headerComponent: () => <>Achievement</>,
field: 'achievementPercentage',
width: 130,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { achievementPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(achievementPercentage);
return (
<TableCellRenderer
value={parsedValue ? `${parsedValue}%` : ''}
ellipsisWrapperClass={
achievementPercentage >= 100 ? 'text-[var(--green-base)]' : 'text-[var(--red-base)]'
}
/>
);
}
},
{
headerComponent: () => (
<>
LMSD <br /> achievement
</>
),
field: 'lmsdAchievedPercentage',
width: 125,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { lmsdAchievedPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(lmsdAchievedPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Non-Enach <br /> cash collected
</>
),
field: 'nonEnachCashCollected',
width: 140,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { nonEnachCashCollected } = params?.data || {};
return (
<CellRendererShortNumber value={nonEnachCashCollected} isCurrency significantDigits={2} />
);
}
}
] as ColDefsType[];
export const AGENCY_PERFORMANCE_COLUMNS = [
{
headerComponent: () => <>Agent</>,
field: 'agentName',
width: 200,
cellRenderer: (params: ICellRendererParams) => {
const { agentName, agencyName } = params.data || {};
return (
<div className="flex flex-col">
<TableCellRenderer value={agentName} toolTipContent={agentName} showToolTip />
<div className="text-[--navi-color-gray-c3] text-xs">
<TableCellRenderer value={agencyName} toolTipContent={agencyName} showToolTip />
</div>
</div>
);
}
},
{
headerComponent: () => (
<>
LANs <br />
allocated
</>
),
field: 'totalAllocatedLans',
width: 90,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalAllocatedLans } = params?.data || {};
const parsedValue = formatNumber(totalAllocatedLans ?? 0);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => (
<>
31-90 <br />
coverage
</>
),
field: 'coverageThirtyOneNinetyLansPercent',
width: 100,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { coverageThirtyOneNinetyLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(coverageThirtyOneNinetyLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
90+ <br />
coverage
</>
),
field: 'coverageNinetyPlusLansPercent',
width: 90,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { coverageNinetyPlusLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(coverageNinetyPlusLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
31-90 genuine <br />
coverage
</>
),
field: 'genuineCoverageThirtyOneNinetyLansPercent',
width: 120,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineCoverageThirtyOneNinetyLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineCoverageThirtyOneNinetyLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
90+ genuine <br />
coverage
</>
),
field: 'genuineCoverageNinetyPlusLansPercent',
width: 130,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineCoverageNinetyPlusLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineCoverageNinetyPlusLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Visits <br />
/day
</>
),
field: 'visitsPerDay',
width: 60,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { visitsPerDay } = params?.data || {};
const parsedValue = toFixedValueNotation(visitsPerDay);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => (
<>
Genuine <br />
visits/day
</>
),
field: 'genuineVisitsPerDay',
width: 90,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineVisitsPerDay } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineVisitsPerDay);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => (
<>
PTPs <br />
generated
</>
),
field: 'ptpsGenerated',
width: 100,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { ptpsGenerated, ptpsGeneratedPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(ptpsGeneratedPercent);
return (
<div className="flex flex-col">
<TableCellRenderer value={ptpsGenerated} />
<div className="text-[--navi-color-gray-c3] text-xs">
<TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />
</div>
</div>
);
}
},
{
headerComponent: () => (
<>
Broken <br />
PTP
</>
),
field: 'brokenPtpPercentage',
width: 70,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { brokenPtpPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(brokenPtpPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Distance <br />
travelled/day
</>
),
field: 'distanceTravelledPerDay',
width: 110,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { distanceTravelledPerDay } = params?.data || {};
const parsedValue = toFixedValueNotation(distanceTravelledPerDay);
return (
<TableCellRenderer
value={
parsedValue
? pluralisation(
`${formatNumber(Number(parsedValue))} km`,
distanceTravelledPerDay,
's'
)
: ''
}
/>
);
}
},
{
headerComponent: () => (
<>
App usage <br />
time/day
</>
),
field: 'appUsagePerDay',
width: 110,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { appUsagePerDay } = params?.data || {};
return <TableCellRenderer value={secondsToHM(appUsagePerDay)} />;
}
}
] as ColDefsType[];
export const TODAY_AGENCY_PERFORMANCE_COLUMNS = [
{
headerComponent: () => <>Agent</>,
field: 'agentName',
width: 240,
cellRenderer: (params: ICellRendererParams) => {
const { agentName, agencyName } = params.data || {};
return (
<div className="flex flex-col">
<TableCellRenderer value={agentName} toolTipContent={agentName} showToolTip />
<div className="text-[--navi-color-gray-c3] text-xs">
<TableCellRenderer value={agencyName} toolTipContent={agencyName} showToolTip />
</div>
</div>
);
}
},
{
headerComponent: () => <>Visits</>,
field: 'visitsToday',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { visitsToday } = params?.data || {};
return <TableCellRenderer value={visitsToday} />;
}
},
{
headerComponent: () => <>Todays PTP</>,
field: 'ptpsToday',
width: 180,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { ptpsToday } = params?.data || {};
return <TableCellRenderer value={ptpsToday} />;
}
},
{
headerComponent: () => <>Todays PTP visit</>,
field: 'ptpsVisitedToday',
width: 150,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { ptpsVisitedToday } = params?.data || {};
return <TableCellRenderer value={ptpsVisitedToday} />;
}
},
{
headerComponent: () => <>Partially/Fully Paid Accounts</>,
field: 'accountsPaidToday',
width: 150,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { accountsPaidToday } = params?.data || {};
return <TableCellRenderer value={accountsPaidToday} />;
}
},
{
headerComponent: () => <>OD collected</>,
field: 'odCollectedToday',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { odCollectedToday } = params?.data || {};
return <CellRendererShortNumber value={odCollectedToday} isCurrency significantDigits={2} />;
}
},
{
headerComponent: () => <>Total cash collected</>,
field: 'cashCollectedToday',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { cashCollectedToday } = params?.data || {};
return (
<CellRendererShortNumber value={cashCollectedToday} isCurrency significantDigits={2} />
);
}
}
] as ColDefsType[];
export const AGENCY_DETAILS_COLUMNS = [
{
headerComponent: () => <>Agency</>,
field: 'agencyName',
width: 200,
cellRenderer: (params: ICellRendererParams) => {
const { agencyName } = params.data || {};
return <TableCellRenderer value={agencyName} toolTipContent={agencyName} showToolTip />;
}
},
{
headerComponent: () => (
<>
31-90 <br />
coverage
</>
),
field: 'coverageThirtyOneNinetyLansPercent',
width: 90,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { coverageThirtyOneNinetyLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(coverageThirtyOneNinetyLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
90+ <br />
coverage
</>
),
field: 'coverageNinetyPlusLansPercent',
width: 90,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { coverageNinetyPlusLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(coverageNinetyPlusLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
31-90 genuine <br />
coverage
</>
),
field: 'genuineCoverageThirtyOneNinetyLansPercent',
width: 90,
wrapHeaderText: true,
wrapText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineCoverageThirtyOneNinetyLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineCoverageThirtyOneNinetyLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
90+ genuine <br />
coverage
</>
),
field: 'genuineCoverageNinetyPlusLansPercent',
width: 110,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineCoverageNinetyPlusLansPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineCoverageNinetyPlusLansPercent);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Visits/agent/ <br />
day
</>
),
field: 'visitsPerDayPerAgent',
width: 120,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { visitsPerDayPerAgent } = params?.data || {};
const parsedValue = toFixedValueNotation(visitsPerDayPerAgent);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => (
<>
Genuine visits <br />
/agent/day
</>
),
field: 'genuineVisitsPerDayPerAgent',
width: 130,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { genuineVisitsPerDayPerAgent } = params?.data || {};
const parsedValue = toFixedValueNotation(genuineVisitsPerDayPerAgent);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => (
<>
PTPs <br />
generated
</>
),
field: 'ptpsGenerated',
width: 110,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { ptpsGenerated, ptpsGeneratedPercent } = params?.data || {};
const parsedValue = toFixedValueNotation(ptpsGeneratedPercent);
return (
<div className="flex flex-col">
<TableCellRenderer value={ptpsGenerated} />
<div className="text-[--navi-color-gray-c3] text-xs">
<TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />
</div>
</div>
);
}
},
{
headerComponent: () => (
<>
Broken <br />
PTP
</>
),
field: 'brokenPtpPercentage',
width: 90,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { brokenPtpPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(brokenPtpPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerComponent: () => (
<>
Distance travelled <br />
/agent/day
</>
),
field: 'distanceTravelledPerDayPerAgent',
width: 140,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { distanceTravelledPerDayPerAgent } = params?.data || {};
const parsedValue = toFixedValueNotation(distanceTravelledPerDayPerAgent);
return (
<TableCellRenderer
value={
parsedValue
? pluralisation(
`${formatNumber(Number(parsedValue))} km`,
distanceTravelledPerDayPerAgent,
's'
)
: ''
}
/>
);
}
},
{
headerComponent: () => (
<>
App usage time/ <br />
agent/day
</>
),
field: 'appUsagePerDayPerAgent',
width: 135,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { appUsagePerDayPerAgent } = params?.data || {};
return <TableCellRenderer value={secondsToHM(appUsagePerDayPerAgent)} />;
}
}
] as ColDefsType[];
export const TODAY_AGENCY_DETAILS_COLUMNS = [
{
headerComponent: () => <>Agency</>,
field: 'agencyName',
width: 240,
cellRenderer: (params: ICellRendererParams) => {
const { agencyName } = params.data || {};
return <TableCellRenderer value={agencyName} toolTipContent={agencyName} showToolTip />;
}
},
{
headerComponent: () => <>Visits/agent</>,
field: 'visitsTodayPerAgent',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { visitsTodayPerAgent } = params?.data || {};
const parsedValue = toFixedValueNotation(visitsTodayPerAgent);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerComponent: () => <>Todays PTP accounts</>,
field: 'totalPtpsToday',
width: 180,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalPtpsToday } = params?.data || {};
return <TableCellRenderer value={totalPtpsToday} />;
}
},
{
headerComponent: () => <>Todays PTP visit</>,
field: 'totalPtpsVisitedToday',
width: 150,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalPtpsVisitedToday } = params?.data || {};
return <TableCellRenderer value={totalPtpsVisitedToday} />;
}
},
{
headerComponent: () => <>Partially/Fully Paid Accounts</>,
field: 'totalAccountsPaidToday',
width: 150,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalAccountsPaidToday } = params?.data || {};
return <TableCellRenderer value={totalAccountsPaidToday} />;
}
},
{
headerComponent: () => <>OD collected</>,
field: 'totalOdCollectedToday',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalOdCollectedToday } = params?.data || {};
return (
<CellRendererShortNumber value={totalOdCollectedToday} isCurrency significantDigits={2} />
);
}
},
{
headerComponent: () => <>Total cash collected</>,
field: 'totalCashCollectedToday',
width: 160,
wrapHeaderText: true,
cellRenderer: (params: ICellRendererParams) => {
const { totalCashCollectedToday } = params?.data || {};
return (
<CellRendererShortNumber value={totalCashCollectedToday} isCurrency significantDigits={2} />
);
}
}
] as ColDefsType[];
export const AGENT_PERFORMANCE_COLUMNS = [
{
field: 'agentName',
headerName: 'Agent Name',
width: 240,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { agentName } = params.data || {};
return <TableCellRenderer value={agentName} toolTipContent={agentName} showToolTip />;
}
},
{
field: 'lansAllocated',
headerName: 'LANs allocated',
width: 160,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { lansAllocated } = params?.data || {};
const parsedValue = formatNumber(lansAllocated ?? 0);
return <TableCellRenderer value={parsedValue} />;
}
},
{
headerName: 'EMI OD',
field: 'emiOverdueAmount',
width: 180,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiOverdueAmount } = params?.data || {};
return <CellRendererShortNumber value={emiOverdueAmount} isCurrency significantDigits={2} />;
}
},
{
headerName: 'Target CE',
field: 'targetCEPercentage',
width: 90,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { targetCEPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(targetCEPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerName: 'Taregt OD amount',
field: 'targetOverdueAmount',
width: 150,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { targetOverdueAmount } = params?.data || {};
return (
<CellRendererShortNumber value={targetOverdueAmount} isCurrency significantDigits={2} />
);
}
},
{
headerName: 'EMI CE',
field: 'emiCEPercentage',
width: 150,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiCEPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(emiCEPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerName: 'EMI OD collected',
field: 'emiOverdueCollected',
width: 160,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { emiOverdueCollected } = params?.data || {};
return (
<CellRendererShortNumber value={emiOverdueCollected} isCurrency significantDigits={2} />
);
}
},
{
headerName: 'Achievement',
field: 'achievementPercentage',
width: 160,
wrapHeaderText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { achievementPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(achievementPercentage);
return (
<TableCellRenderer
value={parsedValue ? `${parsedValue}%` : ''}
ellipsisWrapperClass={
achievementPercentage >= 100 ? 'text-[var(--green-base)]' : 'text-[var(--red-base)]'
}
/>
);
}
},
{
headerName: 'LMSD achievement',
field: 'lmsdAchievedPercentage',
width: 125,
wrapHeaderText: true,
wrapText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { lmsdAchievedPercentage } = params?.data || {};
const parsedValue = toFixedValueNotation(lmsdAchievedPercentage);
return <TableCellRenderer value={parsedValue ? `${parsedValue}%` : ''} />;
}
},
{
headerName: 'Non-Enach cash collected',
field: 'nonEnachCashCollected',
width: 140,
wrapHeaderText: true,
wrapText: true,
sortable: true,
cellRenderer: (params: ICellRendererParams) => {
const { nonEnachCashCollected } = params?.data || {};
return (
<CellRendererShortNumber value={nonEnachCashCollected} isCurrency significantDigits={2} />
);
}
}
] as ColDefsType[];

View File

@@ -0,0 +1,124 @@
import React, { useEffect, useRef } from 'react';
import Loader from 'src/components/Loader/Loader';
import FilterWrapper from '../FilterWrapper';
import RenderAgTable from '../RenderAgTable';
import styles from './styles.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from 'src/store';
import {
getAgencyAndStateOptions,
getDpdBucketData
} from '../../actions/ExternalDashboardSenseiActions';
import { DPD_BUCKET_COLUMNS } from '../ColumnDefs';
import commonStyles from '../commonStyles.module.scss';
import {
alternativeColorStyles,
DPD_BUCKET_TABLE_HEIGHT,
filterTitle,
filterTypeMapping,
rowIndexZeroStyles,
QueryParamTypeMapping,
POLLING_TIME
} from '../../constants';
import { poll } from '@cp/src/utils/polling';
import { noop } from '@navi/web-ui/lib/utils/common';
import cx from 'classnames';
interface IDpdBucketTable {
isVisible: boolean;
}
const DpdBucketTable: React.FC<IDpdBucketTable> = props => {
const { isVisible } = props;
const { dpdBucketTable, agencyNameOptions, stateOptions, featureflags } = useSelector(
(store: RootState) => ({
dpdBucketTable: store?.externalDashboardSensei?.dpdBucketTable,
agencyNameOptions: store?.externalDashboardSensei?.agencyNameOptions,
stateOptions: store?.externalDashboardSensei?.stateOptions,
featureflags: store?.common?.featureFlags
})
);
const { overallPerformanceAgentPerformanceTable } = featureflags;
const { dpdBucketData, loading } = dpdBucketTable || {};
const { data = [] } = dpdBucketData || {};
const dispatch = useDispatch();
const unsubscribe = useRef<() => void>();
useEffect(() => {
if (isVisible) {
// Fetching Agency Name and State Name Dropdown
dispatch(getAgencyAndStateOptions());
unsubscribe.current = poll(
() => dispatch(getDpdBucketData()),
json => json,
POLLING_TIME,
noop
);
}
return () => {
unsubscribe.current?.();
};
}, [isVisible]);
const filterCallbackFn = () => {
if (!overallPerformanceAgentPerformanceTable) {
dispatch(getAgencyAndStateOptions());
}
dispatch(getDpdBucketData());
};
return (
<div className={styles.dpdBucketContainer}>
<div className={cx(styles.tableHeader, 'stickyHeader')}>
<FilterWrapper
title={filterTitle.state}
options={stateOptions}
filterType={filterTypeMapping.stateName}
tableType={QueryParamTypeMapping.DPD_BUCKET_TABLE}
callBackFn={filterCallbackFn}
disabled={loading}
/>
{!overallPerformanceAgentPerformanceTable && (
<FilterWrapper
title={filterTitle.agencyName}
options={agencyNameOptions}
filterType={filterTypeMapping.agencyCode}
tableType={QueryParamTypeMapping.DPD_BUCKET_TABLE}
callBackFn={() => {
dispatch(getDpdBucketData());
}}
disabled={loading}
/>
)}
</div>
<div className="relative">
<RenderAgTable
data={data}
colDefs={DPD_BUCKET_COLUMNS}
tableName={QueryParamTypeMapping.DPD_BUCKET_TABLE}
tableHeight={DPD_BUCKET_TABLE_HEIGHT}
getRowStyle={params => {
// if index is 0 applying different styles
if (params.node.rowIndex === 0) {
return rowIndexZeroStyles;
}
// for alternative color
if (params.node.rowIndex && params.node.rowIndex % 2 === 0) {
return alternativeColorStyles;
}
}}
/>
<Loader show={loading} className={commonStyles.loadingState} animate={false} />
</div>
</div>
);
};
export default DpdBucketTable;

View File

@@ -0,0 +1,9 @@
.dpdBucketContainer {
position: relative;
}
.tableHeader {
padding: 16px 0 8px 0;
top: 0;
display: flex;
}

View File

@@ -0,0 +1,92 @@
import React, { useMemo } from 'react';
import Filter from '@cp/src/components/Filter';
import { CLICKSTREAM_EVENT_NAMES } from '@cp/src/service/clickStream.constant';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { useNavigate, useParams } from 'react-router-dom';
import { SelectPickerOptionProps, SelectPickerValue } from 'src/components/interfaces';
import { createQueryParams, readQueryParams } from 'src/utils/QueryParamsHelper';
import { filterTypeMapping } from '../constants';
import { IFilterWrapper } from '../types';
import styles from './commonStyles.module.scss';
import Loader from '@cp/src/components/Loader/Loader';
const FilterWrapper: React.FC<IFilterWrapper> = props => {
const {
title,
options,
filterType,
tableType,
callBackFn,
isPaginatedTable = false,
disabled = false,
isSingleSelect = true
} = props;
const params = readQueryParams();
const navigate = useNavigate();
const selectedValue = params?.[tableType]?.[filterType];
const selectedLabel = useMemo(
() => options?.find(a => a.value === selectedValue)?.label,
[params, options]
);
const { tabId = '' } = useParams();
const onSelectionChange = (selectedFilter: any) => {
if (!params) return;
let updatedSelectedFilter = '';
if (!isSingleSelect) {
updatedSelectedFilter = selectedFilter?.map((filter: any) => filter.value).join(',');
} else {
updatedSelectedFilter = selectedFilter.value;
}
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_FILTER_APPLIED, {
section: tabId,
filterType: filterType,
oldValue: selectedValue ?? null,
newValue: updatedSelectedFilter ?? null
});
const updatedParams = { ...params };
updatedParams[tableType] = {
...updatedParams[tableType],
[filterType]: updatedSelectedFilter
};
if (updatedParams[tableType].agencyCode && filterType === filterTypeMapping.stateName) {
delete updatedParams[tableType].agencyCode;
}
if (isPaginatedTable) updatedParams[tableType].pageNumber = '1';
const paramsData = createQueryParams(updatedParams);
navigate(paramsData);
callBackFn?.();
};
const onClickHandler = () => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_FILTER_CLICKED, {
section: tabId,
filterType: filterType,
existingValue: selectedValue ?? null
});
};
const selectedValues = selectedValue?.split(',');
return (
<div className="relative">
<Filter
key={`${filterType}_${selectedValue}`}
title={selectedLabel ?? title}
options={options as unknown as SelectPickerOptionProps[]}
onClick={onClickHandler}
onSelectionChange={change => onSelectionChange(change as any)}
isSingleSelect={isSingleSelect}
showSearchBar
filterClass={styles.filterContainer}
containerAppliedClass={styles.titleClass}
selectedValue={selectedValue}
selectedValues={selectedValues}
/>
<Loader show={disabled} className={styles.loadingState} animate={false} />
</div>
);
};
export default FilterWrapper;

View File

@@ -0,0 +1,47 @@
$headerzIndex: 3;
.header {
padding: 24px 24px 16px;
display: flex;
top: 0;
position: sticky;
z-index: $headerzIndex;
background-color: var(--bg-primary);
align-items: center;
justify-content: space-between;
.headerTitle {
display: flex;
align-items: center;
gap: 8px;
}
.lastUpdatedAt {
color: var(--navi-color-gray-c3);
}
.lastRefreshedContainer {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.refresh {
cursor: pointer;
.refreshWrapper {
color: var(--blue-base);
align-items: center;
display: flex;
.refreshIcon {
margin-right: 4px;
fill {
color: var(--blue-base);
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
import LocationIcon from '@cp/src/assets/icons/Location';
import APP_ROUTES from '@cp/src/layout/Routes';
import { LIVE_LOCATION_SOURCE } from '@cp/src/pages/LiveLocationTracker/constants/LiveLocatonTrackerConstants';
import { getLiveLocationParams } from '@cp/src/pages/LiveLocationTracker/utils';
import { showAgentsLiveLocation } from '@cp/src/pages/PerformanceDashboard/PerformanceDashboard';
import {
PERFORMANCE_DASHBOARD_CLICKSTREAM,
PERFORMANCE_DASHBOARD_EVENTS
} from '@cp/src/service/clickStream.constant';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { RootState } from '@cp/src/store';
import { DateFormat, dateFormat } from '@cp/src/utils/DateHelper';
import { RefreshIcon } from '@navi/web-ui/lib/icons';
import { Button, Typography } from '@navi/web-ui/lib/primitives';
import { useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import {
getAgencyDetailsData,
getAgentPerformanceData,
getAgentPerformanceTLData,
getDpdBucketData
} from '../../actions/ExternalDashboardSenseiActions';
import { TabsKey } from '../../constants';
import { getLastUpdatedAt } from '../../utils';
import styles from './index.module.scss';
const Header = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { tabId = '' } = useParams();
const { allLastUpdatedAt, userData, featureFlags } = useSelector((state: RootState) => ({
allLastUpdatedAt: state?.externalDashboardSensei?.allLastUpdatedAt,
userData: state?.common?.userData,
featureFlags: state?.common?.featureFlags
}));
const {
overallPerformanceOverallMetricsTable,
fieldGovernanceAgencyPerformanceTable,
fieldGovernanceAgentPerformanceTable,
overallPerformanceAgentPerformanceTable
} = featureFlags || {};
const lastUpdatedAt = useMemo(
() => getLastUpdatedAt(allLastUpdatedAt, tabId),
[tabId, allLastUpdatedAt]
);
const { roles } = userData || {};
const handleRefresh = () => {
switch (tabId) {
case TabsKey.OVERALL_PERFORMANCE:
if (overallPerformanceOverallMetricsTable) dispatch(getDpdBucketData());
if (overallPerformanceAgentPerformanceTable) dispatch(getAgentPerformanceTLData());
break;
case TabsKey.FIELD_GOVERNANCE:
if (fieldGovernanceAgencyPerformanceTable) dispatch(getAgencyDetailsData());
if (fieldGovernanceAgentPerformanceTable) dispatch(getAgentPerformanceData());
break;
default:
break;
}
};
const enableAgentLiveLocation = useMemo(() => showAgentsLiveLocation(roles), [roles]);
const handleLiveLocationClick = () => {
addClickstreamEvent(
PERFORMANCE_DASHBOARD_CLICKSTREAM[PERFORMANCE_DASHBOARD_EVENTS.LH_AGENT_TRACKING_CLICKED],
{ userId: userData?.referenceId }
);
const updatedParams = getLiveLocationParams(LIVE_LOCATION_SOURCE.EXTERNAL_SENSEI);
navigate(`${APP_ROUTES.LIVE_LOCATION_TRACKER.path}${updatedParams}`);
};
return (
<div className={styles.header}>
<div className={styles.headerTitle}>
<Typography variant="h3">Agent Dashboard</Typography>
<Typography variant="p5" className={styles.lastUpdatedAt}>
Last updated at{' '}
{lastUpdatedAt
? dateFormat(new Date(lastUpdatedAt), DateFormat.LONG_DATE_FORMAT_WITH_TIME)
: '--'}
</Typography>
<div className={styles.refresh} onClick={handleRefresh}>
<RefreshIcon className={styles.refreshIcon} color="var(--navi-color-blue-base)" />
</div>
</div>
<div className={styles.lastRefreshedContainer}>
{enableAgentLiveLocation ? (
<div>
<Button
onClick={handleLiveLocationClick}
startAdornment={<LocationIcon />}
variant="text"
>
Agent location
</Button>
</div>
) : null}
</div>
</div>
);
};
export default Header;

View File

@@ -0,0 +1,102 @@
import { CLICKSTREAM_EVENT_NAMES } from '@cp/src/service/clickStream.constant';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import AgTable from '@navi/web-ui/lib/components/AgTable/AgTable';
import { DropDownPosition } from '@navi/web-ui/lib/components/Pagination/constant';
import Pagination from '@navi/web-ui/lib/components/Pagination/Pagination';
import { AgGridReact } from 'ag-grid-react';
import React, { useEffect, useRef } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { createQueryParams, readQueryParams } from 'src/utils/QueryParamsHelper';
import { IRenderAgTable } from '../types';
import { handleGridReady } from './ColumnDefs';
import styles from './commonStyles.module.scss';
const RenderAgTable: React.FC<IRenderAgTable> = props => {
const { colDefs, data, pageDetails, tableName, tableHeight = 400, callBackFn, ...rest } = props;
const params = readQueryParams();
const navigate = useNavigate();
const location = useLocation();
const gridRef = useRef<AgGridReact>(null);
const { tabId = '' } = useParams();
const handlePageChange = (pageNo: number) => {
if (!params) return;
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_PAGE_CHANGED, {
newPageNo: pageNo,
section: tabId,
oldPageNo: pageDetails?.currentPage,
tableName: tableName
});
const updatedParams = { ...params };
updatedParams[tableName].pageNumber = String(pageNo);
const paramsData = createQueryParams(updatedParams);
navigate(paramsData);
callBackFn?.();
};
const handleSizeChange = (pageSize: number) => {
if (!params) return;
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_PAGE_SIZE_CHANGED, {
newPageSize: pageSize,
section: tabId,
oldPageSize: pageDetails?.pageSize,
tableName: tableName
});
const updatedParams = { ...params };
updatedParams[tableName].pageNumber = '1'; // on change setting it to default page number 1
updatedParams[tableName].pageSize = String(pageSize);
const paramsData = createQueryParams(updatedParams);
navigate(paramsData);
callBackFn?.();
};
useEffect(() => {
if (gridRef?.current?.api?.sizeColumnsToFit) {
gridRef?.current?.api?.sizeColumnsToFit();
}
}, [tabId]);
return (
<div className="isolate">
<AgTable
ref={gridRef}
key={tableName}
className="senseiAgTable"
columnDefs={colDefs}
rowData={data}
onGridReady={e => handleGridReady(e, gridRef)}
defaultColDef={{
suppressMovable: true,
wrapText: true,
autoHeight: true
}}
style={{
height: tableHeight,
marginTop: 8
}}
headerHeight={72}
theme={'alpine'}
sizeColumnsToFit
alternateRowColor="#F7F8FC"
paginationWrapperClasses={styles.paginationWrapperClasses}
PaginationComponent={
pageDetails ? (
<Pagination
key={location.search}
pageSize={pageDetails?.pageSize}
currentPage={pageDetails?.currentPage}
totalCount={pageDetails?.totalElements}
onPageChange={handlePageChange}
onPageSizeChange={handleSizeChange}
pageNumberDropDownPosition={DropDownPosition.TOP}
/>
) : null
}
{...rest}
/>
</div>
);
};
export default RenderAgTable;

View File

@@ -0,0 +1,117 @@
@import '../../../assets/styles/animations.scss';
.handleEllipsis {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
line-height: 24px;
}
.loadingState {
width: 100%;
height: 100%;
position: absolute;
top: 0;
background-color: rgba(255, 255, 255, 0.6);
filter: blur(5px);
animation: fadeIn 120ms;
}
.paginationLoading {
height: calc(100% + 64px); // Pagination Height
}
.tableHeaderTitle {
color: var(--navi-color-gray-c1);
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.2px;
padding: 14px 0 8px 0;
}
.filterContainer {
margin-right: 8px;
}
.paginationWrapperClasses {
overflow: unset;
bottom: 4px;
z-index: 2;
> div:nth-child(1) {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
}
:global {
.senseiAgTable {
.ag-header {
background-color: var(--grayscale-6);
div {
color: var(--greyscale-3) !important;
}
}
.ag-header-row {
.ag-header-cell {
padding-left: 16px;
padding-right: 0px !important;
}
.ag-header-cell:last-child {
padding-right: 16px !important;
}
}
.ag-row {
.ag-cell {
padding-left: 16px;
padding-right: 0px;
}
}
.ag-center-cols-clipper {
min-height: unset !important;
}
.ag-header-group-cell {
border-right: 1px solid var(--navi-color-gray-border);
padding: 0 !important;
margin: 0 !important;
}
.ag-header-group-cell-label {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.ag-header-group-cell {
.ag-header-group-text {
font-size: 14px !important;
}
:nth-child(1) {
display: inline-block;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
}
}
}
.stickyHeader {
position: sticky;
background: var(--bg-primary);
width: 100%;
top: 72px;
z-index: 1;
}
}

View File

@@ -0,0 +1,63 @@
import { MILLI_IN_MIN } from '@cp/src/utils/DateHelper';
export enum QueryParamTypeMapping {
AGENCY_DETAILS_TABLE = 'agencyDetailsTable',
AGENT_PERFORMANCE_TABLE = 'agentPerformanceTable',
DPD_BUCKET_TABLE = 'dpdBucketTable',
FIELD_GOVERNANCE_TAB = 'fieldGovernanceTab',
EXTERNAL_SENSEI = 'externalSensei',
AGENT_PERFORMANCE_TL_TABLE = 'agentPerformanceTlTable'
}
export const filterTypeMapping = {
agencyCode: 'agencyCode',
stateName: 'stateName',
dpdBucket: 'dpdBucket'
};
export const filterTitle = {
agency: 'Agency',
agencyName: 'Select agency',
state: 'Select state',
dpdBucket: 'All DPD Buckets'
};
export enum TabsKey {
OVERALL_PERFORMANCE = 'overall_performance',
FIELD_GOVERNANCE = 'field_governance',
DAILY_PLANNING = 'daily_planning'
}
export const rowIndexZeroStyles = {
fontWeight: 500,
background: 'none',
color: 'var(--navi-color-gray-c1)'
};
export const alternativeColorStyles = {
background: 'var(--blue-light-table)',
fontWeight: 'normal'
};
export const DPD_BUCKET_OPTIONS = [
{
label: 'DPD 31 - 60',
value: '31-60'
},
{
label: 'DPD 61 - 90',
value: '61-90'
},
{
label: 'DPD 91 - 180',
value: '91-180'
},
{
label: 'DPD 180 and above',
value: '180+'
}
];
export const FIELD_GOVERNANCE_TABLE_HEIGHT = 634.75;
export const DPD_BUCKET_TABLE_HEIGHT = 354.75;
export const POLLING_TIME = 30 * MILLI_IN_MIN; // 30 mins;

View File

@@ -0,0 +1,23 @@
.content {
position: relative;
height: calc(100vh - 64px); // Main Header (64px)
}
.tabContainerClass {
box-shadow: 0px 6px 11px 0px rgba(90, 90, 90, 0.08);
position: sticky;
top: 0;
z-index: var(--z-index-external-sensei-header);
background-color: var(--bg-primary);
&:first-child {
padding-left: 24px;
}
}
.tabContent {
padding: 0 24px;
height: fit-content;
height: calc(100vh - 100px); // Main Header (64px) + Tabs Header (36px)
overflow-y: scroll;
}

View File

@@ -0,0 +1,90 @@
import Tabs, { TabItem } from '@navi/web-ui/lib/components/Tabs';
import { useEffect } from 'react';
import { TabItemKey } from '@navi/web-ui/lib/components/Tabs/types';
import { useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import styles from './index.module.scss';
import { getTabs } from './utils';
import { TabsKey } from './constants';
import { RootState } from '@cp/src/store';
import { addClickstreamEvent } from '@cp/src/service/clickStreamEventService';
import { CLICKSTREAM_EVENT_NAMES } from '@cp/src/service/clickStream.constant';
import Header from './components/Header';
import APP_ROUTES from '@cp/src/layout/Routes';
import { interpolatePathParams } from '@cp/src/utils/interpolate';
const ExternalDashboardSensei = () => {
const navigate = useNavigate();
const { tabId = '' } = useParams();
const { featureFlags } = useSelector((state: RootState) => ({
featureFlags: state?.common?.featureFlags
}));
const dashboardTabs = getTabs(featureFlags, tabId);
const handleTabChange = (updateTabId: TabItemKey) => {
if (tabId !== updateTabId) {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_TAB_SWITCH, {
currentTab: tabId,
newTab: updateTabId
});
const updatedUrl = interpolatePathParams(APP_ROUTES.SENSEI_EXTERNAL.path, {
tabId: String(updateTabId)
});
navigate(updatedUrl);
}
};
useEffect(() => {
// If no tab access is there redirect the user to first screen
if (!dashboardTabs.length) {
navigate('/');
return;
}
// Handling the edge case if the url is shared but the user don't have the tab access so selecting the first tab
const isTabAccessNotAvailable = tabId && !dashboardTabs.find(tab => tab.key === tabId);
// And handling the case where tabId is not there and we are selecting the first tab for which user has the access
const noTabSelected = ![
TabsKey.FIELD_GOVERNANCE,
TabsKey.OVERALL_PERFORMANCE,
TabsKey.DAILY_PLANNING
].includes(tabId as TabsKey);
if (isTabAccessNotAvailable || noTabSelected) {
navigate(dashboardTabs?.[0]?.relativePath);
}
}, [tabId, dashboardTabs]);
useEffect(() => {
addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.LH_FIELD_DASHBOARD_PAGE_LAND, {
tab: tabId
});
}, []);
return (
<>
<Header />
<div className={styles.content}>
{tabId && dashboardTabs.length ? (
<Tabs
tabsClassName={styles.tabContainerClass}
selectedTabKey={tabId}
onTabChange={handleTabChange}
contentClassName={styles.tabContent}
preserveContent
>
{dashboardTabs.map(tab => {
return (
<TabItem key={tab.key} label={tab.value}>
{tab.component}
</TabItem>
);
})}
</Tabs>
) : null}
</div>
</>
);
};
export default ExternalDashboardSensei;

View File

@@ -0,0 +1,104 @@
import { createSlice } from '@reduxjs/toolkit';
import { IExternalSenseiDashboard } from '../types';
const initialState: IExternalSenseiDashboard = {
dpdBucketTable: {
dpdBucketData: {},
loading: false
},
agentPerformanceTlTable: {
agentPerformanceTlData: {},
loading: false
},
agencyDetailsTable: {
agencyDetailsMtdData: {},
agencyDetailsTodayData: {},
loading: false
},
agentPerformanceTable: {
agentPerformanceMtdData: {},
agentPerformanceTodayData: {},
loading: false
},
agencyNameOptions: [],
stateOptions: [],
agentsOptions: [],
allLastUpdatedAt: {
dpdBucketLastUpdatedAt: 0,
agencyDetailsLastUpdatedAt: 0,
agentPerformanceLastUpdatedAt: 0,
agentPerformanceTlLastUpdatedAt: 0
}
};
const externalDashboardSenseiSlice = createSlice({
name: 'externalDashboardSensei',
initialState,
reducers: {
setDpdBucketTable: (state, action) => {
const { data } = action.payload;
state.dpdBucketTable.dpdBucketData = data;
},
setLoadingDpdBucket: (state, action) => {
state.dpdBucketTable.loading = action.payload;
},
setAgentPerformanceTlTable: (state, action) => {
const { data } = action.payload;
state.agentPerformanceTlTable.agentPerformanceTlData = data;
},
setLoadingAgentPerformanceTl: (state, action) => {
state.agentPerformanceTlTable.loading = action.payload;
},
setAgencyDetailsTable: (state, action) => {
const { data, isToday } = action.payload;
if (isToday) {
state.agencyDetailsTable.agencyDetailsTodayData = data;
return;
}
state.agencyDetailsTable.agencyDetailsMtdData = data;
},
setLoadingAgencyDetails: (state, action) => {
state.agencyDetailsTable.loading = action.payload;
},
setAgentPerformanceTable: (state, action) => {
const { data, isToday } = action.payload;
if (isToday) {
state.agentPerformanceTable.agentPerformanceTodayData = data;
return;
}
state.agentPerformanceTable.agentPerformanceMtdData = data;
},
setLoadingAgentPerformance: (state, action) => {
state.agentPerformanceTable.loading = action.payload;
},
setAgencyNameOptions: (state, action) => {
state.agencyNameOptions = action.payload;
},
setStateOptions: (state, action) => {
state.stateOptions = action.payload;
},
setAgentsOptions: (state, action) => {
state.agentsOptions = action.payload;
},
setTablesLastUpdatedAt: (state, action) => {
state.allLastUpdatedAt = { ...state.allLastUpdatedAt, ...action.payload };
}
}
});
export const {
setDpdBucketTable,
setLoadingDpdBucket,
setAgentPerformanceTlTable,
setLoadingAgentPerformanceTl,
setAgencyDetailsTable,
setLoadingAgencyDetails,
setAgentPerformanceTable,
setLoadingAgentPerformance,
setAgencyNameOptions,
setStateOptions,
setAgentsOptions,
setTablesLastUpdatedAt
} = externalDashboardSenseiSlice.actions;
export default externalDashboardSenseiSlice.reducer;

View File

@@ -0,0 +1,194 @@
import { IPagination } from '@cp/src/components/interfaces';
import { AgTableProps, ColDefsType } from '@navi/web-ui/lib/components/AgTable/types';
import { QueryParamTypeMapping } from './constants';
export enum FIELD_GOVERNANCE_TABS {
MTD = 'MTD',
TODAY = 'Today'
}
export enum FIELD_GOVERNANCE_TABS_VALUE {
MTD = 'MTD',
TODAY = 'TODAY'
}
export interface IDpdBucketData {
dpdBucket: string;
performanceBand: number;
lansAllocated: number;
agentAllocationPercentage: number;
emiOverdueAmount: number;
targetCEPercentage: number;
targetOverdueAmount: number;
emiCEPercentage: number;
emiOverdueCollected: number;
achievementPercentage: number;
lmsdAchievedPercentage: number;
nonEnachCashCollected: number;
}
export interface IDpdBucketDataTable {
data?: IDpdBucketData;
}
export interface IDpdBucketTable {
dpdBucketData: IDpdBucketDataTable;
loading: boolean;
}
export interface IAgentPerformanceTlData {
agentName: string;
lansAllocated: number;
emiOverdueAmount: number;
targetCEPercentage: number;
targetOverdueAmount: number;
emiCEPercentage: number;
emiOverdueCollected: number;
achievementPercentage: number;
lmsdAchievedPercentage: number;
nonEnachCashCollected: number;
}
export interface IAgentPerformanceTlDataTable {
data?: IAgentPerformanceTlData;
}
export interface IAgentPerformanceTlTable {
agentPerformanceTlData: IAgentPerformanceTlDataTable;
loading: boolean;
}
export interface IAgencyDetailsMtdData {
agencyName?: string;
agencyCode?: string;
coverageThirtyNinetyLansPercent?: number;
coverageNinetyOneEightyLansPercent?: number;
genuineCoverageThirtyNinetyLansPercent?: number;
genuineCoverageNinetyOneEightyLansPercent?: number;
visitsPerDayPerAgent?: number;
genuineVisitsPerDayPerAgent?: number;
ptpsGenerated?: number;
distanceTravelledPerDayPerAgent?: number;
appUsagePerDayPerAgent?: number;
brokenPtpPercentage?: number;
}
export interface IAgencyDetailsMtdTable {
data?: IAgencyDetailsMtdData;
pages?: IPagination;
}
export interface IAgencyDetailsTodayData {
agencyName: string;
agencyCode: string;
visitsTodayPerAgent: number;
totalPtpsToday: number;
totalPtpsVisitedToday: number;
totalAccountsPaidToday: number;
totalOdCollectedToday: number;
totalCashCollectedToday: number;
}
export interface IAgencyDetailsTodayTable {
data?: IAgencyDetailsTodayData;
pages?: IPagination;
}
export interface IAgencyDetailsTable {
agencyDetailsMtdData: IAgencyDetailsMtdTable;
agencyDetailsTodayData: IAgencyDetailsTodayTable;
loading: boolean;
}
export interface IAgentPerformanceMtdData {
agentName: string;
agentReferenceId: string;
totalLansAllocated: number;
coverageThirtyNinetyLansPercent: number;
coverageNinetyOneEightyLansPercent: number;
genuineCoverageThirtyNinetyLansPercent: number;
genuineCoverageNinetyOneEightyLansPercent: number;
visitsPerDay: number;
genuineVisitsPerDay: number;
ptpsGenerated: number;
distanceTravelledPerDay: number;
appUsagePerDay: number;
brokenPtpPercentage: number;
}
export interface IAgentPerformanceMtdTable {
data?: IAgentPerformanceMtdData;
pages?: IPagination;
}
export interface IAgentPerformanceTodayData {
agentName: string;
agentReferenceId: string;
visitsToday: number;
ptpsToday: number;
ptpsVisitedToday: number;
accountsPaidToday: number;
odCollectedToday: number;
cashCollectedToday: number;
}
export interface IAgentPerformanceTodayTable {
data?: IAgentPerformanceTodayData;
pages?: IPagination;
}
export interface IAgentPerformanceTable {
agentPerformanceMtdData: IAgentPerformanceMtdTable;
agentPerformanceTodayData: IAgentPerformanceTodayTable;
loading: boolean;
}
export interface IOption {
label: string;
value: string;
}
export interface IAllLastUpdateAt {
dpdBucketLastUpdatedAt: number;
agencyDetailsLastUpdatedAt: number;
agentPerformanceLastUpdatedAt: number;
agentPerformanceTlLastUpdatedAt: number;
}
export interface IExternalSenseiDashboard {
dpdBucketTable: IDpdBucketTable;
agentPerformanceTlTable: IAgentPerformanceTlTable;
agencyDetailsTable: IAgencyDetailsTable;
agentPerformanceTable: IAgentPerformanceTable;
agencyNameOptions: Array<IOption>;
stateOptions: Array<IOption>;
agentsOptions: Array<IOption>;
allLastUpdatedAt: IAllLastUpdateAt;
}
export interface IPageDetails {
totalElements: number;
currentPage: number;
pageSize: number;
}
export interface IRenderAgTable extends AgTableProps {
colDefs: ColDefsType[];
// Ag Table expects any so declared it as any
data: any;
tableName: string;
tableHeight: number;
pageDetails?: IPageDetails;
callBackFn?: () => void;
}
export interface IFilterWrapper {
title: string;
options: IOption[];
filterType: string;
tableType: QueryParamTypeMapping;
callBackFn: () => void;
isPaginatedTable?: boolean;
disabled?: boolean;
isSingleSelect?: boolean;
}

View File

@@ -0,0 +1,130 @@
import { IFeatureFlags } from '@cp/src/reducers/commonSlice';
import { createQueryParams, IData } from '@cp/src/utils/QueryParamsHelper';
import { NavigateFunction } from 'react-router-dom';
import { QueryParamTypeMapping, TabsKey } from './constants';
import FieldGovernance from './FieldGovernance';
import OverallPerformance from './OverallPerformance';
import { FIELD_GOVERNANCE_TABS_VALUE, IAllLastUpdateAt } from './types';
export const getTabs = (featureFlags: IFeatureFlags, tabId: string) => {
const { overallPerformanceTab, fieldGovernanceTab } = featureFlags || {};
const tabs = [];
if (overallPerformanceTab) {
tabs.push({
key: TabsKey.OVERALL_PERFORMANCE,
value: 'Overall Performance',
component: <OverallPerformance />,
relativePath: '/sensei-external/overall_performance'
});
}
if (fieldGovernanceTab) {
tabs.push({
key: TabsKey.FIELD_GOVERNANCE,
value: 'Field Governance',
component: <FieldGovernance />,
relativePath: '/sensei-external/field_governance'
});
}
return tabs;
};
export const setInitialGovernanceTabParams = (
params: IData,
navigate: NavigateFunction,
featureFlags: IFeatureFlags
) => {
const { fieldGovernanceAgencyPerformanceTable, fieldGovernanceAgentPerformanceTable } =
featureFlags || {};
const queryParams = { ...params };
if (fieldGovernanceAgencyPerformanceTable) {
if (!queryParams?.[QueryParamTypeMapping.AGENCY_DETAILS_TABLE])
queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE] = {};
if (!queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE]?.pageNumber) {
queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE].pageNumber = '1';
}
if (!queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE]?.pageSize) {
queryParams[QueryParamTypeMapping.AGENCY_DETAILS_TABLE].pageSize = '10';
}
}
if (fieldGovernanceAgentPerformanceTable) {
if (!queryParams?.[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE])
queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE] = {};
if (!queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE]?.pageNumber) {
queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE].pageNumber = '1';
}
if (!queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE]?.pageSize) {
queryParams[QueryParamTypeMapping.AGENT_PERFORMANCE_TABLE].pageSize = '10';
}
}
if (!queryParams?.[QueryParamTypeMapping.FIELD_GOVERNANCE_TAB])
queryParams[QueryParamTypeMapping.FIELD_GOVERNANCE_TAB] = {};
if (!queryParams[QueryParamTypeMapping.FIELD_GOVERNANCE_TAB]?.selectedTab) {
queryParams[QueryParamTypeMapping.FIELD_GOVERNANCE_TAB].selectedTab =
FIELD_GOVERNANCE_TABS_VALUE.MTD;
}
const updatedParams = createQueryParams(queryParams);
navigate(updatedParams);
};
export const setInitialOverallPerformanceTabParams = (
params: IData,
navigate: NavigateFunction,
featureFlags: IFeatureFlags
) => {
const queryParams = { ...params };
const { overallPerformanceOverallMetricsTable } = featureFlags || {};
if (overallPerformanceOverallMetricsTable) {
if (!queryParams?.[QueryParamTypeMapping.DPD_BUCKET_TABLE])
queryParams[QueryParamTypeMapping.DPD_BUCKET_TABLE] = {};
if (!queryParams[QueryParamTypeMapping.DPD_BUCKET_TABLE]?.stateName) {
queryParams[QueryParamTypeMapping.DPD_BUCKET_TABLE].stateName = 'ALL';
}
if (!queryParams[QueryParamTypeMapping.DPD_BUCKET_TABLE]?.agencyCode) {
queryParams[QueryParamTypeMapping.DPD_BUCKET_TABLE].agencyCode = 'ALL';
}
}
const updatedParams = createQueryParams(queryParams);
navigate(updatedParams);
};
export const getLastUpdatedAt = (allLastUpdatedAt: IAllLastUpdateAt, tabId: string) => {
const {
dpdBucketLastUpdatedAt,
agencyDetailsLastUpdatedAt,
agentPerformanceLastUpdatedAt,
agentPerformanceTlLastUpdatedAt
} = allLastUpdatedAt;
if (tabId === TabsKey.OVERALL_PERFORMANCE) {
if (!dpdBucketLastUpdatedAt) return agentPerformanceTlLastUpdatedAt;
if (!agentPerformanceTlLastUpdatedAt) return dpdBucketLastUpdatedAt;
if (dpdBucketLastUpdatedAt < agentPerformanceTlLastUpdatedAt) {
return dpdBucketLastUpdatedAt;
}
return agentPerformanceTlLastUpdatedAt;
}
if (tabId === TabsKey.FIELD_GOVERNANCE) {
if (!agencyDetailsLastUpdatedAt) return agentPerformanceLastUpdatedAt;
if (!agentPerformanceLastUpdatedAt) return agencyDetailsLastUpdatedAt;
if (agencyDetailsLastUpdatedAt < agentPerformanceLastUpdatedAt) {
return agencyDetailsLastUpdatedAt;
}
return agentPerformanceLastUpdatedAt;
}
};

View File

@@ -7,7 +7,10 @@ import RefreshIcon from '../../../../assets/icons/RefreshIcon';
import { Button } from '@primitives';
import { FilterTypes } from '../../constants/LiveLocationTrackerInterfaces';
import { useDispatch, useSelector } from 'react-redux';
import { AGENT_LIVE_LOCATION } from '../../constants/LiveLocatonTrackerConstants';
import {
AGENT_LIVE_LOCATION,
LIVE_LOCATION_SOURCE
} from '../../constants/LiveLocatonTrackerConstants';
import { createQueryParams, readQueryParams } from '../../../../utils/QueryParamsHelper';
import { getAgentsLocations } from '../../actions/LiveLocationTrackerActions';
import { RootState } from '../../../../store';
@@ -35,6 +38,11 @@ function TopBar() {
if (!queryParams?.AGENTID) {
// clear selected agent and map pin locations
dispatch(resetMapData());
if (queryParams?.SOURCE === LIVE_LOCATION_SOURCE.EXTERNAL_SENSEI) {
navigate(APP_ROUTES.SENSEI_EXTERNAL.relativePath);
return;
}
navigate(APP_ROUTES.PERFORMANCE_DASHBOARD.path);
return;
}

View File

@@ -83,3 +83,8 @@ export const locationsPinFilters = [
export const DEFAULT_TEXT = '--';
export const MIN_CHAR_LENGTH_FOR_TOOLTIP = 20;
export enum LIVE_LOCATION_SOURCE {
EXTERNAL_SENSEI = 'external_sensei',
PERFORMANCE_DASHBOARD = 'performance_dashboard'
}

View File

@@ -15,6 +15,7 @@ import { NavigateFunction } from 'react-router-dom';
import { formatEpochTime } from 'src/utils/DateHelper';
import styles from './components/AgentMarker/agentMarker.module.scss';
import { v4 as uuidv4 } from 'uuid';
import { LocalStorage } from '@cp/src/utils/StorageUtils';
export const getAllocationPins = (
data: Record<string, IAgentAllocation>,
@@ -218,3 +219,21 @@ export const loadCustomPopup = (type?: string) => {
}
return CustomPopup;
};
export const getLiveLocationParams = (source: string) => {
// Get the current live-location-tracker-params from local storage
const currentParams = LocalStorage.getItem('live-location-tracker-params') || '';
const paramRegex = /SOURCE\*AGENT_LIVE_LOCATION=[^&]*/;
const hasParam = paramRegex.test(currentParams);
let updatedParams;
if (hasParam) {
updatedParams = currentParams.replace(paramRegex, `SOURCE*${AGENT_LIVE_LOCATION}=${source}`);
} else {
updatedParams =
currentParams + (currentParams ? '&' : '?') + `SOURCE*${AGENT_LIVE_LOCATION}=${source}`;
}
return updatedParams;
};

View File

@@ -45,10 +45,12 @@ import {
import { Button } from '@primitives';
import LocationIcon from '../../assets/icons/Location';
import Routes from '../../layout/Routes';
import { LIVE_LOCATION_SOURCE } from '../LiveLocationTracker/constants/LiveLocatonTrackerConstants';
import { getLiveLocationParams } from '../LiveLocationTracker/utils';
export const PERFORMANCE_DASHBOARD = 'performanceDashboard';
const showAgentsLiveLocation = (roles: string[] | undefined) => {
export const showAgentsLiveLocation = (roles: string[] | undefined) => {
let show = false;
if (!roles) return show;
// for loop that returns flag as soon as it finds the role
@@ -401,11 +403,9 @@ const PerformanceDashboard = () => {
PERFORMANCE_DASHBOARD_CLICKSTREAM[PERFORMANCE_DASHBOARD_EVENTS.LH_AGENT_TRACKING_CLICKED],
{ userId: user?.referenceId }
);
// navigate to the old url if live-location-tracker-params is there
navigate(
Routes.LIVE_LOCATION_TRACKER.path +
(localStorage.getItem('live-location-tracker-params') || '')
);
const updatedParams = getLiveLocationParams(LIVE_LOCATION_SOURCE.PERFORMANCE_DASHBOARD);
// Navigate to the new URL with the updated parameters
navigate(Routes.LIVE_LOCATION_TRACKER.path + updatedParams);
};
const enableAgentLiveLocation = useMemo(() => showAgentsLiveLocation(roles), [roles]);

View File

@@ -219,6 +219,10 @@ export interface GenerateAmeyoPassword {
email: string;
}
export interface IFeatureFlags {
[key: string]: boolean;
}
export interface CommonState {
userData: User;
toast?: ToastData;
@@ -242,7 +246,8 @@ export interface CommonState {
isInternalTeamLead?: boolean;
isCallingAgent?: boolean;
feedbackFilters: string;
serverDate: Date | string;
serverDate: Date;
featureFlags: IFeatureFlags;
isAmeyoUtilityVisible?: boolean;
isAmeyoGeneratePasswordVisible?: boolean;
}
@@ -286,6 +291,7 @@ const initialState = {
isAmeyoUtilityVisible: false,
isAmeyoGeneratePasswordVisible: false,
isIdCardApprovalVisible: false,
featureFlags: {},
humanReminderCustomerDetailsLoading: false,
generateAmeyoPassword: {
ameyoKey: '',
@@ -312,12 +318,8 @@ export const commonSlice = createSlice({
const roles = action.payload?.roles;
const isNaviUser = action.payload?.naviUser;
if (roles?.length) {
const agencyPerformanceDashboardVisible =
roles?.includes(Roles.ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD) ||
roles?.includes(Roles.ROLE_NAVI_FIELD_TEAM_LEAD) ||
roles?.includes(Roles.ROLE_NAVI_FIELD_AGENCY_TEAM_LEAD);
state.isPerformanceDashboardVisible = agencyPerformanceDashboardVisible;
state.isPerformanceDashboardVisible =
action.payload?.featureFlags?.performanceDashboard ?? false;
const isIdCardApprovalVisible =
roles?.includes(Roles.ROLE_NAVI_FIELD_EXTERNAL_TEAM_LEAD) ||
@@ -328,6 +330,8 @@ export const commonSlice = createSlice({
const isAmeyoGeneratePasswordVisible =
action.payload?.featureFlags?.ameyoGeneratePasswordFlag ?? false;
state.featureFlags = action.payload?.featureFlags;
state.isIdCardApprovalVisible = isIdCardApprovalVisible;
state.isAmeyoUtilityVisible = isAmeyoUtilityVisible;
state.isAmeyoGeneratePasswordVisible = isAmeyoGeneratePasswordVisible;

View File

@@ -573,6 +573,36 @@ export const CLICKSTREAM_EVENT_NAMES = Object.freeze({
LH_AMEYO_PONG_RECEIVED_LONGHORN: {
name: 'LH_AMEYO_PONG_RECEIVED_LONGHORN',
description: 'When pong is received from longhorn.'
},
// External Dashboard - Sensei
LH_FIELD_DASHBOARD_FILTER_CLICKED: {
name: 'LH_FIELD_DASHBOARD_FILTER_CLICKED',
description: 'Field dashboard filter clicked'
},
LH_FIELD_DASHBOARD_FILTER_APPLIED: {
name: 'LH_FIELD_DASHBOARD_FILTER_APPLIED',
description: 'Field dashboard filter applied'
},
LH_FIELD_DASHBOARD_PAGE_CHANGED: {
name: 'LH_FIELD_DASHBOARD_PAGE_CHANGED',
description: 'Field dashboard page changed'
},
LH_FIELD_DASHBOARD_PAGE_SIZE_CHANGED: {
name: 'LH_FIELD_DASHBOARD_PAGE_SIZE_CHANGED',
description: 'Field dashboard page size changed'
},
LH_FIELD_DASHBOARD_TAB_SWITCH: {
name: 'LH_FIELD_DASHBOARD_TAB_SWITCH',
description: 'Field dashboard tab switch'
},
LH_FIELD_DASHBOARD_PAGE_LAND: {
name: 'LH_FIELD_DASHBOARD_PAGE_LAND',
description: 'Field dashboard page land'
},
LH_FIELD_DASHBOARD_GOVERNANCE_TAB_SWITCH: {
name: 'LH_FIELD_DASHBOARD_GOVERNANCE_TAB_SWITCH',
description: 'Field dashboard governance tab switch'
}
});

View File

@@ -25,6 +25,7 @@ import ameyoUtilitySlice from '../pages/AmeyoUtility/reducer/ameyoUtilitySlice';
import whatsappChatbotSlice from '../reducers/whatsappChatbotSlice';
import TeleSenseiSlice from '@cp/src/pages/SenseiTele/reducer';
import Sensei from '../pages/sensei/reducers';
import ExternalDashboardSenseiSlice from 'src/pages/ExternalDashboardSensei/reducers/ExternalDashboardSenseiSlice';
export const reducer = combineReducers({
common: commonReducer,
@@ -41,6 +42,7 @@ export const reducer = combineReducers({
reallocation: reallocationSlice,
teamLeadDashboard: TeamLeadSlice,
liveLocationTracker: LiveLocationTrackerSlice,
externalDashboardSensei: ExternalDashboardSenseiSlice,
agentAvailabilitySlice: AgentAvailabilitySlice,
leaveManagement: LeaveManagementSlice,
agencyPincodeMapping: agencyPincodeMappingSlice,

View File

@@ -160,7 +160,13 @@ export enum ApiKeys {
CONVERT_TIFF_TO_JPEG,
LEGAL_DOCS,
GET_DEPOSITIONS,
REGISTER_FCM_TOKEN
REGISTER_FCM_TOKEN,
GET_DPD_BUCKET_DATA,
GET_AGENCY_NAME_AND_STATE_OPTIONS,
GET_AGENCY_NAME_OPTIONS,
GET_AGENCY_DETAILS_DATA,
GET_AGENT_PERFORMANCE_DATA,
GET_AGENT_PERFORMANCE_TL_DATA
}
// TODO: try to get rid of `as`
@@ -305,6 +311,18 @@ API_URLS[ApiKeys.GET_AGENT_PERFORMANCE] = '/team-lead/dashboard/reportees-perfor
API_URLS[ApiKeys.CONVERT_TIFF_TO_JPEG] = '/v1/documents/convert/tiff-to-jpeg';
API_URLS[ApiKeys.REGISTER_FCM_TOKEN] = '/fcm';
API_URLS[ApiKeys.GET_DEPOSITIONS] = '/team-lead/dashboard/reportees-disposition-summary';
API_URLS[ApiKeys.GET_DPD_BUCKET_DATA] =
'/longhorn/external-agency-performance-dashboard/overall-performance/overall-metrics';
API_URLS[ApiKeys.GET_AGENCY_NAME_AND_STATE_OPTIONS] =
'/longhorn/external-agency-performance-dashboard/overall-performance/overall-metrics/filter';
API_URLS[ApiKeys.GET_AGENCY_NAME_OPTIONS] =
'/longhorn/external-agency-performance-dashboard/field-governance/reporting-agency/filter';
API_URLS[ApiKeys.GET_AGENCY_DETAILS_DATA] =
'/longhorn/external-agency-performance-dashboard/field-governance/agency';
API_URLS[ApiKeys.GET_AGENT_PERFORMANCE_DATA] =
'/longhorn/external-agency-performance-dashboard/field-governance/agent';
API_URLS[ApiKeys.GET_AGENT_PERFORMANCE_TL_DATA] =
'/longhorn/external-agency-performance-dashboard/overall-performance/agent-performance';
// TODO: try to get rid of `as`
const MOCK_API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
@@ -338,6 +356,12 @@ MOCK_API_URLS[ApiKeys.ACTIVE_TELE_USERS] = 'activeTeleUsers.json';
MOCK_API_URLS[ApiKeys.GET_REALLOCATION_HISTORY] = 'reallocationHistory.json';
MOCK_API_URLS[ApiKeys.AGENT_CASES_LIST] = 'agentCasesList.json';
MOCK_API_URLS[ApiKeys.AGENT_LOCATIONS] = 'agentLocations.json';
MOCK_API_URLS[ApiKeys.GET_DPD_BUCKET_DATA] = 'dpdBucket.json';
MOCK_API_URLS[ApiKeys.GET_AGENCY_NAME_AND_STATE_OPTIONS] = 'agencyAndStateOptions.json';
MOCK_API_URLS[ApiKeys.GET_AGENCY_NAME_OPTIONS] = 'agencyOptions.json';
MOCK_API_URLS[ApiKeys.GET_AGENCY_DETAILS_DATA] = 'agencyDetails.json';
MOCK_API_URLS[ApiKeys.GET_AGENT_PERFORMANCE_DATA] = 'agentPerformance.json';
MOCK_API_URLS[ApiKeys.GET_AGENT_PERFORMANCE_TL_DATA] = 'agentPerformanceTl.json';
let navigate: NavigateFunction;
let dispatch: Dispatch<any>;
@@ -486,9 +510,11 @@ const excludedRedirectionEndPointsList = [
'/cases/details/lan',
APP_ROUTES.AGENT_AVAILABILITY.path,
APP_ROUTES.SENSEI_TELE.path,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSAI.daily_planning,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.daily_planning,
APP_ROUTES.AMEYO_UTILITY.path,
APP_ROUTES.AMEYO_UPLOAD_NUMBERS.path
APP_ROUTES.AMEYO_UPLOAD_NUMBERS.path,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.overall_performance,
APP_ROUTES_PATHS_WITH_PATH_PARAM.SENSEI.field_governance
];
const checkIsValidRedirection = () => {

View File

@@ -1,7 +1,7 @@
import { IFilterSchema } from '../components/interfaces';
type IconvertIntoMap = IterableIterator<[string, string]>;
type IData = {
export type IData = {
/* eslint-disable @typescript-eslint/no-explicit-any */
[key: string]: any;
};

View File

@@ -397,3 +397,15 @@ export const getFileDownload = (url: string) => {
downloadLink.click();
document.body.removeChild(downloadLink);
};
export const toFixedValueNotation = (value: string, decimal = 2) => {
if (!value) return '0';
const parsedValue = Number(value);
const result = isWholeNumber(parsedValue)
? Math.trunc(parsedValue).toString()
: parsedValue.toFixed(decimal);
return result.replace(/\.0$/, ''); // Remove trailing .0 for whole numbers
};